Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: voice attachment #125

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/assets/icons/Icon32PauseCircle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/assets/icons/Icon32PlayCircle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions src/assets/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,7 @@ export { default as Icon24ViewOutline } from './Icon24ViewOutline.svg'
export { default as Icon24VolumeOutline } from './Icon24VolumeOutline.svg'
export { default as Icon28DeleteOutline } from './Icon28DeleteOutline.svg'
export { default as Icon32DonutCircleFillYellow } from './Icon32DonutCircleFillYellow.svg'
export { default as Icon32PauseCircle } from './Icon32PauseCircle.svg'
export { default as Icon32PlayCircle } from './Icon32PlayCircle.svg'
export { default as Icon32Spinner } from './Icon32Spinner.svg'
export { default as Icon44Spinner } from './Icon44Spinner.svg'
21 changes: 21 additions & 0 deletions src/converters/AttachConverter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,27 @@ export function fromApiAttaches(apiAttaches: MessagesMessageAttachment[]): Attac
break
}

case 'audio_message': {
if (!apiAttach.audio_message) {
addUnknown(apiAttach)
break
}

attaches.voice = {
kind: 'Voice',
id: apiAttach.audio_message.id,
ownerId: Peer.resolveOwnerId(apiAttach.audio_message.owner_id),
accessKey: apiAttach.audio_message.access_key,
linkMp3: apiAttach.audio_message.link_mp3,
linkOgg: apiAttach.audio_message.link_ogg,
duration: apiAttach.audio_message.duration,
waveform: apiAttach.audio_message.waveform,
transcript: apiAttach.audio_message.transcript,
transcriptState: apiAttach.audio_message.transcript_state
}
break
}

case 'photo': {
if (!apiAttach.photo?.orig_photo) {
addUnknown(apiAttach)
Expand Down
2 changes: 1 addition & 1 deletion src/lang/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ export const ru = {
me_message_attach_wall: 'Запись',
me_message_attach_wall_reply: 'Комментарий',
me_message_attach_event: 'Мероприятие',
me_message_attach_audio_message: 'Аудиосообщение',
me_message_attach_voice: 'Аудиосообщение',
me_message_attach_audio_playlist: 'Плейлист',
me_message_attach_artist: 'Исполнитель',
me_message_attach_curator: 'Куратор',
Expand Down
2 changes: 1 addition & 1 deletion src/misc/dateTime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ function copyDate(date: Date) {
return new Date(date.getTime())
}

function startOfDay(date: Date) {
export function startOfDay(date: Date) {
const copy = new Date(date.getTime())
danyadev marked this conversation as resolved.
Show resolved Hide resolved
copy.setHours(0, 0, 0, 0)
return copy
Expand Down
21 changes: 19 additions & 2 deletions src/model/Attach.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type Attaches = {
sticker?: Sticker
photos?: NonEmptyArray<Photo>
links?: NonEmptyArray<Link>
voice?: Voice
wall?: Wall
unknown?: NonEmptyArray<Unknown>
}
Expand Down Expand Up @@ -37,6 +38,19 @@ export type Link = {
imageSizes?: ImageSizes
}

export type Voice = {
kind: 'Voice'
id: number
ownerId: Peer.OwnerId
accessKey: string
linkMp3: string
linkOgg: string
duration: number
waveform: number[]
transcript: string
transcriptState: 'in_progress' | 'done' | 'error'
}

export type Wall = {
kind: 'Wall'
id: number
Expand Down Expand Up @@ -111,7 +125,8 @@ export function preview(attach: Attach, lang: ILang.Lang): string {

switch (attach.kind) {
case 'Sticker':
case 'Wall': {
case 'Wall':
case 'Voice': {
const lowerCaseName = attach.kind.toLowerCase() as Lowercase<typeof attach.kind>
return lang.use(`me_message_attach_${lowerCaseName}`)
}
Expand All @@ -132,13 +147,15 @@ function previewUnknown(unknown: NonNullable<Attaches['unknown']>, lang: ILang.L
unknown.filter(({ type }) => type === firstType).length
)

case 'audio_message':
return lang.use('me_message_attach_voice')

case 'gift':
case 'sticker':
case 'ugc_sticker':
case 'wall':
case 'wall_reply':
case 'event':
case 'audio_message':
case 'audio_playlist':
case 'artist':
case 'curator':
Expand Down
14 changes: 13 additions & 1 deletion src/model/api-types/objects/MessagesMessageAttachment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export type MessagesMessageAttachment = {
market_album?: unknown
call?: unknown
graffiti?: unknown
audio_message?: unknown
audio_message?: MessagesMessageAttachmentAudioMessage
artist?: unknown
event?: unknown
mini_app?: unknown
Expand Down Expand Up @@ -144,6 +144,18 @@ type MessagesMessageAttachmentLink = {
photo?: PhotosPhoto
}

type MessagesMessageAttachmentAudioMessage = {
id: number
owner_id: number
access_key: string
link_mp3: string
link_ogg: string
duration: number
waveform: number[]
transcript: string
transcript_state: 'in_progress' | 'done' | 'error'
}

export type MessagesMessageAttachmentWall = {
inner_type: 'wall_wallpost'
id?: number
Expand Down
31 changes: 31 additions & 0 deletions src/ui/messenger/attaches/AttachVoice/AttachVoice.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
.AttachVoice__top {
display: flex;
gap: 8px;
color: var(--vkui--color_background_accent_themed);
}

.AttachVoice__range {
appearance: none;
cursor: pointer;
height: 3px;
background-color: var(--vkui--color_text_secondary);
overflow: hidden;
}

.AttachVoice__range::-webkit-slider-thumb {
appearance: none;
height: 3px;
width: 2px;
cursor: pointer;
box-shadow: -100px 0 0 100px var(--vkui--color_background_accent_themed);
}

.AttachVoice__time {
display: block;
padding: 2px 0;
font: var(--messageDateFontSize) / var(--messageDateLineHeight) var(--fontFamily);
}

.AttachVoice__details {
color: black;
}
108 changes: 108 additions & 0 deletions src/ui/messenger/attaches/AttachVoice/AttachVoice.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { defineComponent, InputEvent, shallowRef } from 'vue'
import * as Attach from 'model/Attach'
import { useEnv } from 'hooks'
import { startOfDay } from 'misc/dateTime'
import { ButtonIcon } from 'ui/ui/ButtonIcon/ButtonIcon'
import { Icon32PauseCircle, Icon32PlayCircle } from 'assets/icons'
import './AttachVoice.css'

type Props = {
voice: Attach.Voice
}

export const AttachVoice = defineComponent<Props>((props) => {
const { lang } = useEnv()

const audio = new Audio(props.voice.linkMp3)
const isPause = shallowRef(false)
const range = shallowRef(0)
const isRange = shallowRef(false)
const requestId = shallowRef(-1)

const getCurrentTime = () => {
const durationVoice = lang.dateTimeFormatter({ minute: '2-digit', second: '2-digit' }).format(
startOfDay(new Date()).setSeconds(props.voice.duration)
)

const currentTime = lang.dateTimeFormatter({ minute: '2-digit', second: '2-digit' }).format(
startOfDay(new Date()).setSeconds(audio.currentTime)
)

return audio.currentTime === 0 ? durationVoice : currentTime
}

const moveRange = (event: InputEvent<HTMLInputElement>) => {
const currentNumber = (+event.target.value / 100) * props.voice.duration
audio.currentTime = currentNumber
danyadev marked this conversation as resolved.
Show resolved Hide resolved

requestId.value = requestAnimationFrame(updateRange)
}

const updateRange = () => {
if (isRange.value) {
return
}

const min = 0
danyadev marked this conversation as resolved.
Show resolved Hide resolved

const diff = props.voice.duration - min
const currentPosition = audio.currentTime - min
const percentage = (currentPosition / diff) * 100

range.value = percentage

requestId.value = requestAnimationFrame(updateRange)
}

const toggleAudio = () => {
if (!isPause.value) {
requestId.value = requestAnimationFrame(updateRange)
isPause.value = true
audio.play()
return
}

cancelAnimationFrame(requestId.value)
audio.pause()
isPause.value = false
}

audio.onended = () => {
cancelAnimationFrame(requestId.value)
isPause.value = false
range.value = 0
}

return () => {
return (
danyadev marked this conversation as resolved.
Show resolved Hide resolved
<div class="AttachVoice">
<div class="AttachVoice__top">
<ButtonIcon
onClick={toggleAudio}
icon={ isPause.value ? <Icon32PauseCircle /> : <Icon32PlayCircle /> }
danyadev marked this conversation as resolved.
Show resolved Hide resolved
/>
<div class="AttachVoice__track">
<input
class="AttachVoice__range"
type="range"
id="track"
name="track"
value={range.value}
min="0"
max="100"
onChange={(event) => moveRange(event)}
onTouchstart={() => (isRange.value = true)}
onTouchend={() => (isRange.value = false)}
onMousedown={() => (isRange.value = true)}
onMouseup={() => (isRange.value = false)}
/>
<span class="AttachVoice__time">{getCurrentTime().slice(1)}</span>
</div>
</div>
<div class="AttachVoice__bottom">Тут будет открывашка.</div>
</div>
)
}
}, {
props: ['voice']
})
2 changes: 2 additions & 0 deletions src/ui/messenger/attaches/Attaches.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { defineComponent } from 'vue'
import * as Attach from 'model/Attach'
import { useEnv } from 'hooks'
import { ClassName } from 'misc/utils'
import { AttachVoice } from './AttachVoice/AttachVoice'
import { AttachLink } from 'ui/messenger/attaches/AttachLink/AttachLink'
import { AttachPhotos } from 'ui/messenger/attaches/AttachPhotos/AttachPhotos'
import { AttachSticker } from 'ui/messenger/attaches/AttachSticker/AttachSticker'
Expand All @@ -22,6 +23,7 @@ export const Attaches = defineComponent<Props>((props) => {
{props.attaches.photos && <AttachPhotos photos={props.attaches.photos} />}
{props.attaches.links?.map((link) => <AttachLink link={link} />)}
{props.attaches.wall && <AttachWall wall={props.attaches.wall} />}
{props.attaches.voice && <AttachVoice voice={props.attaches.voice} />}
{props.attaches.unknown?.map((unknown) => (
<div class="Attaches__unknown">
{lang.use('me_unknown_attach')} ({unknown.type})
Expand Down
1 change: 1 addition & 0 deletions src/vue-jsx-events.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ declare module 'vue' {

interface InputHTMLAttributes {
onKeydown?: (event: KeyboardEvent<HTMLInputElement>) => void
onChange?: (event: InputEvent<HTMLInputElement>) => void
onInput?: (event: InputEvent<HTMLInputElement>) => void
}

Expand Down