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 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 3 additions & 0 deletions src/assets/icons/Icon16Text.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/assets/icons/Icon20ChevronUp.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/assets/icons/Icon24ChevronDown.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions src/assets/icons/Icon32PauseCircle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { defineComponent, useId } from 'vue'

type Props = {
withUnlistenedDot?: boolean
}

export const Icon32PauseCircle = defineComponent<Props>((props) => {
const id = useId()

return () => (
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
{props.withUnlistenedDot && (
<mask id={id}>
<rect width="100%" height="100%" fill="white" />
<circle cx="27" cy="27" r="5" />
</mask>
)}
<path mask={`url(#${id})`} d="M32 16a16 16 0 1 1-32 0 16 16 0 0 1 32 0zm-20.9-5.45c-.1.21-.1.49-.1 1.05v8.8c0 .56 0 .84.1 1.05a1 1 0 0 0 .45.44c.21.11.49.11 1.05.11h.8c.56 0 .84 0 1.05-.1a1 1 0 0 0 .44-.45c.11-.21.11-.49.11-1.05v-8.8c0-.56 0-.84-.1-1.05a1 1 0 0 0-.45-.44c-.21-.11-.49-.11-1.05-.11h-.8c-.56 0-.84 0-1.05.1a1 1 0 0 0-.44.45zm6 0c-.1.21-.1.49-.1 1.05v8.8c0 .56 0 .84.1 1.05a1 1 0 0 0 .45.44c.21.11.49.11 1.05.11h.8c.56 0 .84 0 1.05-.1a1 1 0 0 0 .44-.45c.11-.21.11-.49.11-1.05v-8.8c0-.56 0-.84-.1-1.05a1 1 0 0 0-.45-.44c-.21-.11-.49-.11-1.05-.11h-.8c-.56 0-.84 0-1.05.1a1 1 0 0 0-.44.45z" fill="currentColor" />
{props.withUnlistenedDot && <circle cx="27" cy="27" r="3" fill="currentColor" />}
</svg>
)
}, {
props: ['withUnlistenedDot']
})
24 changes: 24 additions & 0 deletions src/assets/icons/Icon32PlayCircle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { defineComponent, useId } from 'vue'

type Props = {
withUnlistenedDot?: boolean
}

export const Icon32PlayCircle = defineComponent<Props>((props) => {
const id = useId()

return () => (
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
{props.withUnlistenedDot && (
<mask id={id}>
<rect width="100%" height="100%" fill="white" />
<circle cx="27" cy="27" r="5" />
</mask>
)}
<path mask={`url(#${id})`} clip-rule="evenodd" d="M32 16c0 8.837-7.163 16-16 16S0 24.837 0 16 7.163 0 16 0s16 7.163 16 16zm-9.851.874a1.005 1.005 0 0 0 0-1.739l-8.644-4.994a1.003 1.003 0 0 0-1.505.87v9.988c0 .773.836 1.256 1.505.87z" fill="currentColor" fill-rule="evenodd" />
{props.withUnlistenedDot && <circle cx="27" cy="27" r="3" fill="currentColor" />}
</svg>
)
}, {
props: ['withUnlistenedDot']
})
4 changes: 3 additions & 1 deletion src/assets/icons/Icon32Spinner.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion src/assets/icons/Icon44Spinner.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/assets/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ export { default as Icon16Muted } from './Icon16Muted.svg'
export { default as Icon16Pin } from './Icon16Pin.svg'
export { default as Icon16Repost } from './Icon16Repost.svg'
export { default as Icon16Spinner } from './Icon16Spinner.svg'
export { default as Icon16Text } from './Icon16Text.svg'
export { default as Icon20BombOutline } from './Icon20BombOutline.svg'
export { default as Icon20ChevronUp } from './Icon20ChevronUp.svg'
export { default as Icon20TrashOutline } from './Icon20TrashOutline.svg'
export { default as Icon24Cancel } from './Icon24Cancel.svg'
export { default as Icon24ChevronCompactLeft } from './Icon24ChevronCompactLeft.svg'
// export { default as Icon24ChevronDown } from './Icon24ChevronDown.svg'
// export { default as Icon24ChevronRight } from './Icon24ChevronRight.svg'
export { default as Icon24DoorArrowRightOutline } from './Icon24DoorArrowRightOutline.svg'
export { default as Icon24HideOutline } from './Icon24HideOutline.svg'
Expand All @@ -25,5 +28,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 { Icon32PauseCircle } from './Icon32PauseCircle'
export { Icon32PlayCircle } from './Icon32PlayCircle'
export { default as Icon32Spinner } from './Icon32Spinner.svg'
export { default as Icon44Spinner } from './Icon44Spinner.svg'
29 changes: 27 additions & 2 deletions src/converters/AttachConverter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import * as Peer from 'model/Peer'
import { insertPeers } from 'actions'
import { isNonEmptyArray } from 'misc/utils'

export function fromApiAttaches(apiAttaches: MessagesMessageAttachment[]): Attach.Attaches {
export function fromApiAttaches(
apiAttaches: MessagesMessageAttachment[],
wasVoiceMessageListened: boolean
): Attach.Attaches {
const attaches: Attach.Attaches = {}

const addUnknown = (attach: MessagesMessageAttachment) => {
Expand Down Expand Up @@ -39,6 +42,28 @@ 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,
wasListened: wasVoiceMessageListened,
transcript: apiAttach.audio_message.transcript?.trim() ?? '',
transcriptState: apiAttach.audio_message.transcript_state
}
break
}

case 'photo': {
if (!apiAttach.photo?.orig_photo) {
addUnknown(apiAttach)
Expand Down Expand Up @@ -122,7 +147,7 @@ function fromApiAttachWall(apiWall: MessagesMessageAttachmentWall): Attach.Wall
accessKey: apiWall.access_key,
createdAt: (apiWall.date ?? 0) * 1000,
text: apiWall.text ?? '',
attaches: fromApiAttaches(apiWall.attachments ?? []),
attaches: fromApiAttaches(apiWall.attachments ?? [], true),
isDeleted: !!apiWall.is_deleted,
donutPlaceholder: apiWall.donut?.placeholder?.text,
repost: apiWall.copy_history?.[0]
Expand Down
6 changes: 3 additions & 3 deletions src/converters/MessageConverter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export function fromApiMessage(message: MessagesMessage): Message.Message {
return {
kind: 'Normal',
text: message.text,
attaches: fromApiAttaches(message.attachments ?? []),
attaches: fromApiAttaches(message.attachments ?? [], !!message.was_listened),
replyMessage: message.reply_message && fromApiForeignMessage(
message.reply_message,
baseMessage.peerId,
Expand Down Expand Up @@ -164,7 +164,7 @@ function fromApiForeignMessage(
authorId: Peer.resolveOwnerId(foreignMessage.from_id),
sentAt: foreignMessage.date * 1000,
text: foreignMessage.text,
attaches: fromApiAttaches(foreignMessage.attachments ?? []),
attaches: fromApiAttaches(foreignMessage.attachments ?? [], !!foreignMessage.was_listened),
replyMessage: foreignMessage.reply_message && fromApiForeignMessage(
foreignMessage.reply_message,
rootPeerId,
Expand Down Expand Up @@ -211,7 +211,7 @@ export function fromApiPinnedMessage(pinnedMessage: MessagesPinnedMessage): Mess
authorId: Peer.resolveOwnerId(pinnedMessage.from_id),
sentAt: pinnedMessage.date * 1000,
text: pinnedMessage.text,
attaches: fromApiAttaches(pinnedMessage.attachments ?? []),
attaches: fromApiAttaches(pinnedMessage.attachments ?? [], true),
replyMessage: pinnedMessage.reply_message && fromApiForeignMessage(
pinnedMessage.reply_message,
peerId,
Expand Down
6 changes: 5 additions & 1 deletion src/lang/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ export const ru = {
me_chat_leaved_status: 'Вы вышли из чата',
me_chat_kicked_status: 'Вы были исключены из чата',

me_voice_transcription_error: 'Ошибка составления транскрипции',
me_voice_transcription_in_progress: 'Расшифровка...',
me_voice_transcript_empty: 'Слова не распознаны',

me_chat_members_count: {
one: '{count} участник',
few: '{count} участника',
Expand Down Expand Up @@ -241,7 +245,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
22 changes: 20 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,20 @@ 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[]
wasListened: boolean
transcript: string
transcriptState: 'in_progress' | 'done' | 'error'
}

export type Wall = {
kind: 'Wall'
id: number
Expand Down Expand Up @@ -111,7 +126,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 +148,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
2 changes: 1 addition & 1 deletion src/model/api-types/objects/MessagesForeignMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ export type MessagesForeignMessage = {
fwd_messages?: MessagesForeignMessage[]
geo?: MessagesMessageAttachmentGeo
id?: number
was_listened?: boolean
peer_id?: number
reply_message?: MessagesForeignMessage
text: string
update_time?: number
was_listened?: boolean
was_played?: boolean
payload?: string
is_expired?: boolean
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
99 changes: 99 additions & 0 deletions src/ui/messenger/attaches/AttachVoice/AttachVoice.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
.AttachVoice__player {
display: flex;
gap: 8px;
}

.AttachVoice__playButton {
color: var(--vkui--color_text_accent_themed);
}

.AttachVoice__track {
flex-grow: 1;
}

.AttachVoice__range {
position: relative;
width: 100%;
appearance: none;
cursor: pointer;
height: 3px;
background: none;
overflow: hidden;
}

.AttachVoice__range::before {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: var(--vkui--color_text_secondary);
opacity: 0.2;
}

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

.AttachVoice__toggleTranscription {
align-self: flex-start;
height: 20px;
width: 30px;
color: var(--vkui--color_text_accent_themed);
border-radius: 8px;
margin-top: 2px;
padding: 2px 6px;
}

.AttachVoice__toggleTranscription::before {
content: '';
border-radius: inherit;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: var(--vkui--color_background_accent);
opacity: 0.1;
transition: opacity var(--fastTransition);
}

.AttachVoice__toggleTranscription:hover::before {
opacity: 0.2;
}

.AttachVoice__time {
font: var(--messageDateFontSize) / var(--messageDateLineHeight) var(--fontFamily);
color: var(--vkui--color_text_accent_themed);
}

.AttachVoice__transcript {
position: relative;
display: flex;
margin-top: 8px;
padding-left: 12px;
}

.AttachVoice__transcript--notReady {
opacity: 0.4;
}

.AttachVoice__transcript::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 2px;
height: 100%;
border-radius: 2px;
background: var(--vkui--color_stroke_accent);
}

.AttachVoice__transcript:not(.AttachVoice__transcript--notReady)::before {
opacity: 0.4;
}
Loading