Skip to content

Commit

Permalink
feat: ✨ Add voice message (beta)
Browse files Browse the repository at this point in the history
  • Loading branch information
SupertigerDev committed Jan 4, 2024
1 parent db7d5b4 commit dad6d30
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 2 deletions.
114 changes: 114 additions & 0 deletions src/components/message-pane/MessagePane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { getStorageObject, setStorageObject, StorageKeys } from '@/common/localS
import { randomKaomoji } from '@/common/kaomoji';
import { MessageLogArea } from './message-log-area/MessageLogArea';
import { TenorImage } from '@/chat-api/services/TenorService';
import { useMicRecorder } from '@nerimity/solid-opus-media-recorder';

export default function MessagePane(props: { mainPaneEl: HTMLDivElement }) {
const params = useParams<{ channelId: string, serverId?: string }>();
Expand Down Expand Up @@ -351,6 +352,10 @@ function CustomTextArea(props: CustomTextAreaProps) {
maxLength={2000}
class={styles.textArea}
/>
<MicButton onBlob={(blob) => {
const file = new File([blob], "voice.ogg", {type: "audio/ogg"});
channelProperties.setAttachment(params.channelId, file);
}} />
<Button
class={classNames(styles.inputButtons, "emojiPickerButton")}
onClick={props.onEmojiPickerClick}
Expand All @@ -372,6 +377,115 @@ function CustomTextArea(props: CustomTextAreaProps) {
)
}


const MicButton = (props: {onBlob?: (blob: Blob) => void}) => {
const {isMobileAgent} = useWindowProperties();
let timer: number | null = null;
let recordStartAt = 0;
let recordEndAt = 0;

const [isRecording, setRecording] = createSignal(false);
const {record, stop} = useMicRecorder();
const [currentDuration, setDuration] = createSignal("0:00");
const [cancelRecording, setCancelRecording] = createSignal(false);


const onTouchMove = (event: TouchEvent) => {
if (!isMobileAgent()) return;
const myLocation = event.changedTouches[0];
const realTarget = document.elementFromPoint(myLocation.clientX, myLocation.clientY);
setCancelRecording(!realTarget?.closest(".voice-recorder-button"))

}

const onMicHold = async () => {
if (isRecording()) return;
recordStartAt = Date.now();

setRecording(true);
const blob = await record();

const durationMs = recordEndAt - recordStartAt;
if (durationMs < 800) return;
if (cancelRecording()) return;

props.onBlob?.(blob);

setRecording(false);
setCancelRecording(false);
}

const onMicRelease = () => {
if (!isRecording()) return;

setTimeout(() => {
recordEndAt = Date.now();
stop();
setRecording(false);
}, 200);
}

onMount(() => {
document.addEventListener("pointerup", onMicRelease);

onCleanup(() => {
document.removeEventListener("pointerup", onMicRelease);
})
})


createEffect(on(isRecording, () => {

if (isRecording()) {
timer = window.setInterval(() => {
const durationMs = Date.now() - recordStartAt;
setDuration(msToTime(durationMs));
}, 1000);
return;
}

if (timer) {
window.clearInterval(timer);
timer = null;
}
setDuration("0:00");
}))


return (
<div style={{"display": "flex", "align-items": "center", "gap": "4px", "align-self": "end"}}>
<Show when={isRecording()}>
<div style={{"font-size": "12px"}}>{currentDuration()}</div>
</Show>
<Button
styles={{"touch-action": "none", "user-select": "none"}}
class={classNames(styles.inputButtons, "voice-recorder-button")}
onPointerDown={onMicHold}
onTouchMove={onTouchMove}
onContextMenu={e => e.preventDefault()}
onPointerEnter={() => !isMobileAgent() && setCancelRecording(false)}
onPointerLeave={() => !isMobileAgent() && setCancelRecording(true)}
iconName={cancelRecording() && isRecording() ? "delete" : "mic"}
padding={[8, 15, 8, 15]}
margin={[3, 0, 3, 3]}
iconSize={18}
primary={isRecording()}
color={cancelRecording() && isRecording() ? "var(--alert-color)" : undefined}
/>
</div>
)
}

function msToTime(duration: number): string {
let seconds: number | string = Math.floor((duration / 1000) % 60),
minutes: number | string = Math.floor((duration / (1000 * 60)) % 60),
hours: number | string = Math.floor((duration / (1000 * 60 * 60)) % 24);

seconds = (seconds < 10) ? "0" + seconds : seconds;

return (hours !== 0 ? hours + ":" : "") + minutes + ":" + seconds;
}

interface TypingPayload {
userId: string;
channelId: string;
Expand Down
7 changes: 6 additions & 1 deletion src/components/message-pane/message-item/MessageItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ function Embeds(props: { message: Message, hovered: boolean }) {


const allowedVideoMimes = ["video/mp4", "video/webm"];
const allowedAudioMimes = ["audio/mp3", "audio/mpeg"];
const allowedAudioMimes = ["audio/mp3", "audio/mpeg", "audio/ogg"];


const GoogleDriveEmbeds = (props: { attachment: RawAttachment }) => {
Expand Down Expand Up @@ -492,6 +492,11 @@ const AudioEmbed = (props: { attachment: RawAttachment }) => {
audio.onloadedmetadata = () => {
setPreloaded(true);
}

audio.onended = () => {
setPlaying(false);
}

audio.src = fileItem.webContentLink!
})
createEffect(() => {
Expand Down
9 changes: 8 additions & 1 deletion src/components/ui/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import Text from './Text';
interface Props {
onMouseLeave?: (event: MouseEvent) => void;
onMouseEnter?: (event: MouseEvent) => void;
onPointerEnter?: (event: PointerEvent) => void;
onPointerLeave?: (event: PointerEvent) => void;
onPointerMove?: (event: PointerEvent) => void;
onTouchMove?: (event: TouchEvent) => void;
onTouchStart?: (event: TouchEvent) => void;
color?: string;
class?: string;
label?: string;
Expand All @@ -15,6 +20,8 @@ interface Props {
textSize?: number;
iconName?: string;
onClick?: (event: MouseEvent) => void;
onPointerDown?: (event: PointerEvent) => void;
onPointerUp?: (event: PointerEvent) => void;
onContextMenu?: (event: MouseEvent) => void;
primary?: boolean;
customChildren?: JSXElement
Expand Down Expand Up @@ -67,7 +74,7 @@ export default function Button(props: Props) {


return (
<ButtonContainer tabindex={props.tabIndex} padding={props.padding} margin={props.margin} style={style()} class={`${props.class} button`} onClick={props.onClick} onContextMenu={props.onContextMenu} onMouseEnter={props.onMouseEnter} onMouseLeave={props.onMouseLeave}>
<ButtonContainer onTouchStart={props.onTouchStart} ontouchmove={props.onTouchMove} onPointerMove={props.onPointerMove} onPointerEnter={props.onPointerEnter} onPointerLeave={props.onPointerLeave} onPointerDown={props.onPointerDown} onPointerUp={props.onPointerUp} tabindex={props.tabIndex} padding={props.padding} margin={props.margin} style={style()} class={`${props.class} button`} onClick={props.onClick} onContextMenu={props.onContextMenu} onMouseEnter={props.onMouseEnter} onMouseLeave={props.onMouseLeave}>
{props.customChildrenLeft && props.customChildrenLeft}
{ props.iconName && <Icon size={props.iconSize} name={props.iconName} color={props.primary ? 'white' : color()} /> }
{ props.label && <Text size={props.textSize || 14} class='label' color={props.primary ? 'white' : color()}>{props.label}</Text> }
Expand Down

0 comments on commit dad6d30

Please sign in to comment.