Skip to content

Commit 6d0b495

Browse files
committed
homepage chat: make voice notes nicer
1 parent 7b2a440 commit 6d0b495

File tree

10 files changed

+459
-141
lines changed

10 files changed

+459
-141
lines changed

hyperdrive/packages/homepage/chat/src/lib.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -463,8 +463,6 @@ async fn send_push_notification_for_group_message(
463463
impl ChatState {
464464
#[init]
465465
async fn initialize(&mut self) {
466-
add_to_homepage("Chat", Some(ICON), Some("/"), None);
467-
468466
// Initialize with default profile
469467
if self.profile.name == "User" {
470468
let our_node = our().node.clone();

hyperdrive/packages/homepage/ui/src/components/Chat/FileUpload.css

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,30 @@
88
display: flex;
99
align-items: flex-end;
1010
z-index: 1000;
11+
opacity: 0;
12+
animation: file-upload-overlay-in 220ms ease forwards;
13+
}
14+
15+
.file-upload-overlay.closing {
16+
animation: file-upload-overlay-out 220ms ease forwards;
1117
}
1218

1319
.file-upload-menu {
1420
background: var(--background);
21+
color: var(--text-primary);
1522
width: 100%;
1623
border-top-left-radius: 12px;
1724
border-top-right-radius: 12px;
18-
padding: 20px;
25+
padding: 20px 20px calc(20px + env(safe-area-inset-bottom, 0px));
1926
display: flex;
2027
flex-direction: column;
2128
gap: 10px;
29+
transform: translateY(100%);
30+
animation: file-upload-menu-in 220ms cubic-bezier(0.2, 0.9, 0.2, 1) forwards;
31+
}
32+
33+
.file-upload-overlay.closing .file-upload-menu {
34+
animation: file-upload-menu-out 220ms cubic-bezier(0.4, 0.0, 1, 1) forwards;
2235
}
2336

2437
.upload-option {
@@ -29,6 +42,8 @@
2942
font-size: 16px;
3043
cursor: pointer;
3144
transition: background 0.2s;
45+
color: var(--text-primary);
46+
text-align: left;
3247
}
3348

3449
.upload-option:active {
@@ -41,6 +56,7 @@
4156
gap: 8px;
4257
width: 100%;
4358
cursor: pointer;
59+
color: inherit;
4460
}
4561

4662
.upload-option .material-symbols-outlined {
@@ -102,3 +118,50 @@
102118
color: #ff4d4d;
103119
margin-top: 4px;
104120
}
121+
122+
@keyframes file-upload-overlay-in {
123+
from {
124+
opacity: 0;
125+
}
126+
to {
127+
opacity: 1;
128+
}
129+
}
130+
131+
@keyframes file-upload-overlay-out {
132+
from {
133+
opacity: 1;
134+
}
135+
to {
136+
opacity: 0;
137+
}
138+
}
139+
140+
@keyframes file-upload-menu-in {
141+
from {
142+
transform: translateY(100%);
143+
}
144+
to {
145+
transform: translateY(0%);
146+
}
147+
}
148+
149+
@keyframes file-upload-menu-out {
150+
from {
151+
transform: translateY(0%);
152+
}
153+
to {
154+
transform: translateY(100%);
155+
}
156+
}
157+
158+
@media (prefers-reduced-motion: reduce) {
159+
.file-upload-overlay,
160+
.file-upload-menu,
161+
.file-upload-overlay.closing,
162+
.file-upload-overlay.closing .file-upload-menu {
163+
animation: none;
164+
opacity: 1;
165+
transform: translateY(0);
166+
}
167+
}

hyperdrive/packages/homepage/ui/src/components/Chat/FileUpload.tsx

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState, useRef, useCallback } from 'react';
1+
import React, { useState, useRef, useCallback, useEffect } from 'react';
22
import './FileUpload.css';
33
import { useChatStore } from '../../store/chat';
44
import * as Caller from '#caller-utils';
@@ -13,12 +13,31 @@ interface UploadStatus {
1313
}
1414

1515
const { upload_file } = Caller.Chat;
16+
const CLOSE_ANIMATION_MS = 220;
1617

1718
const FileUpload: React.FC<FileUploadProps> = ({ onClose }) => {
1819
const { activeChat, settings } = useChatStore();
1920
const [isUploading, setIsUploading] = useState(false);
21+
const [isClosing, setIsClosing] = useState(false);
2022
const [uploadStatus, setUploadStatus] = useState<{ [filename: string]: UploadStatus }>({});
2123
const pendingUploadsRef = useRef(0);
24+
const closeTimeoutRef = useRef<number | null>(null);
25+
26+
useEffect(() => {
27+
return () => {
28+
if (closeTimeoutRef.current !== null) {
29+
window.clearTimeout(closeTimeoutRef.current);
30+
}
31+
};
32+
}, []);
33+
34+
const requestClose = useCallback(() => {
35+
if (isClosing) return;
36+
setIsClosing(true);
37+
closeTimeoutRef.current = window.setTimeout(() => {
38+
onClose();
39+
}, CLOSE_ANIMATION_MS);
40+
}, [isClosing, onClose]);
2241

2342
const readFileAsBase64 = useCallback((file: File): Promise<string> => {
2443
return new Promise((resolve, reject) => {
@@ -111,14 +130,26 @@ const FileUpload: React.FC<FileUploadProps> = ({ onClose }) => {
111130
// Check if all uploads succeeded (no errors)
112131
const hasErrors = Object.values(uploadStatus).some(s => s.error);
113132
if (!hasErrors) {
114-
onClose();
133+
requestClose();
115134
}
116135
}, 1000);
117136
};
118137

138+
const handleMenuClick = (e: React.MouseEvent<HTMLDivElement>) => {
139+
const target = e.target as HTMLElement;
140+
const isActionClick = Boolean(target.closest('button, label, input'));
141+
e.stopPropagation();
142+
if (!isActionClick) {
143+
requestClose();
144+
}
145+
};
146+
119147
return (
120-
<div className="file-upload-overlay" onClick={onClose}>
121-
<div className="file-upload-menu" onClick={(e) => e.stopPropagation()}>
148+
<div
149+
className={`file-upload-overlay ${isClosing ? 'closing' : ''}`}
150+
onClick={requestClose}
151+
>
152+
<div className="file-upload-menu" onClick={handleMenuClick}>
122153
{/* Show upload progress if uploading */}
123154
{Object.keys(uploadStatus).length > 0 && (
124155
<div className="upload-progress-container">
@@ -176,7 +207,7 @@ const FileUpload: React.FC<FileUploadProps> = ({ onClose }) => {
176207
/>
177208
</label>
178209
</button>
179-
<button className="upload-option" onClick={onClose}>
210+
<button className="upload-option" onClick={requestClose}>
180211
Cancel
181212
</button>
182213
</>

hyperdrive/packages/homepage/ui/src/components/Chat/Message.css

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -358,24 +358,46 @@
358358
}
359359

360360
/* Voice note / audio player styles */
361-
.dm-audio-wrapper {
362-
background: var(--surface);
363-
border-radius: 12px;
364-
padding: 8px 12px;
365-
margin: 2px 0;
366-
border: 1px solid rgba(255, 255, 255, 0.05);
361+
.message.audio-message {
362+
max-width: min(86%, 360px);
363+
}
364+
365+
.message-content.message-content--audio {
366+
min-width: 240px;
367+
padding: 8px 56px 18px 10px;
368+
}
369+
370+
.message.other .message-content.message-content--audio {
371+
padding: 8px 48px 18px 10px;
367372
}
368373

369374
.dm-audio-player {
370375
width: 100%;
371376
min-width: 220px;
377+
max-width: 320px;
372378
height: 32px;
373379
border: none;
374380
outline: none;
375381
background: transparent;
376382
display: block;
377383
}
378384

385+
.dm-audio-loading {
386+
font-size: 12px;
387+
opacity: 0.75;
388+
padding: 2px 0;
389+
}
390+
391+
@media (max-width: 420px) {
392+
.message-content.message-content--audio {
393+
min-width: 220px;
394+
}
395+
396+
.dm-audio-player {
397+
min-width: 200px;
398+
}
399+
}
400+
379401
/* Image messages */
380402
.message-image {
381403
max-width: 100%;
@@ -448,10 +470,6 @@
448470
border-color: rgba(0, 0, 0, 0.1);
449471
}
450472

451-
.dm-audio-wrapper {
452-
border-color: rgba(0, 0, 0, 0.05);
453-
}
454-
455473
.payment-event-card,
456474
.message.own .message-content .payment-event-card,
457475
.message.other .message-content .payment-event-card {

hyperdrive/packages/homepage/ui/src/components/Chat/Message.tsx

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -591,7 +591,7 @@ const Message: React.FC<MessageProps> = ({ message, isOwn, spacingClass = 'wide'
591591
<div
592592
ref={messageRef}
593593
id={`message-${message.id}`}
594-
className={`message ${isOwn ? 'own' : 'other'} ${isOfficial ? 'official-message' : ''} ${isPaymentEvent ? 'payment-event' : ''} ${isSwiping ? 'swiping' : ''} spacing-${spacingClass}`}
594+
className={`message ${isOwn ? 'own' : 'other'} ${isAudioMessage ? 'audio-message' : ''} ${isOfficial ? 'official-message' : ''} ${isPaymentEvent ? 'payment-event' : ''} ${isSwiping ? 'swiping' : ''} spacing-${spacingClass}`}
595595
onContextMenu={handleLongPress}
596596
onTouchStart={handleTouchStart}
597597
onTouchMove={handleTouchMove}
@@ -623,7 +623,7 @@ const Message: React.FC<MessageProps> = ({ message, isOwn, spacingClass = 'wide'
623623
</div>
624624
)}
625625

626-
<div className="message-content">
626+
<div className={`message-content ${isAudioMessage ? 'message-content--audio' : ''}`}>
627627
{/* If this is a file/image message with file info, show it specially */}
628628
{isPaymentEvent && paymentInfo ? (
629629
<a
@@ -646,17 +646,15 @@ const Message: React.FC<MessageProps> = ({ message, isOwn, spacingClass = 'wide'
646646
</div>
647647
</a>
648648
) : isAudioMessage && message.file_info ? (
649-
<div className="dm-audio-wrapper">
650-
{audioUrl ? (
651-
<audio
652-
controls
653-
src={audioUrl}
654-
className="dm-audio-player"
655-
/>
656-
) : (
657-
<div style={{ fontSize: '12px', opacity: 0.7 }}>Loading audio…</div>
658-
)}
659-
</div>
649+
audioUrl ? (
650+
<audio
651+
controls
652+
src={audioUrl}
653+
className="dm-audio-player"
654+
/>
655+
) : (
656+
<div className="dm-audio-loading">Loading audio…</div>
657+
)
660658
) : message.file_info && message.message_type === 'Image' && settings?.show_images ? (
661659
<div>
662660
<img

hyperdrive/packages/homepage/ui/src/components/Chat/MessageInput.tsx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import React, { useState, useRef, useEffect } from 'react';
2-
import * as Caller from '#caller-utils';
32
import { useChatStore } from '../../store/chat';
43
import FileUpload from './FileUpload';
54
import VoiceNote from './VoiceNote';
@@ -14,7 +13,15 @@ const MessageInput: React.FC<MessageInputProps> = ({ chatId, onSendMessage }) =>
1413
const [message, setMessage] = useState('');
1514
const [showFileUpload, setShowFileUpload] = useState(false);
1615
const [showVoiceNote, setShowVoiceNote] = useState(false);
17-
const { sendMessage, replyingTo, setReplyingTo, editingMessage, setEditingMessage, editMessage } = useChatStore();
16+
const {
17+
sendMessage,
18+
sendVoiceNote,
19+
replyingTo,
20+
setReplyingTo,
21+
editingMessage,
22+
setEditingMessage,
23+
editMessage,
24+
} = useChatStore();
1825
const inputRef = useRef<HTMLTextAreaElement>(null);
1926
const containerRef = useRef<HTMLDivElement>(null);
2027

@@ -83,12 +90,7 @@ const MessageInput: React.FC<MessageInputProps> = ({ chatId, onSendMessage }) =>
8390
const handleSendVoiceNote = async (payload: { base64: string; duration: number; mimeType: string }) => {
8491
const replyToId = replyingTo?.id || null;
8592
setReplyingTo(null);
86-
await Caller.Chat.send_voice_note({
87-
chat_id: chatId,
88-
audio_data: payload.base64,
89-
duration: payload.duration,
90-
reply_to: replyToId,
91-
});
93+
await sendVoiceNote(chatId, payload.base64, payload.duration, replyToId);
9294
onSendMessage?.();
9395
};
9496

0 commit comments

Comments
 (0)