feat(settings/mobile): Voice & Video adaption + chat header username normalization
MobileChatScreen DM header now applies parseFederatedUsername and
useCanonicalUserView, matching MobileDmsScreen so federated users render
as 'realname' rather than 'realname@domain'.
Mobile Voice & Audio renamed to Voice & Video across MobileSettingsScreen
and MobileYouScreen — parity with desktop's VoicePanel title.
AudioInputSection and VideoSection picker click-outside now listens for
touchstart alongside mousedown so a single tap dismisses on iOS Safari
(which doesn't synthesize mousedown reliably from touch).
AudioOutputSection feature-detects HTMLMediaElement.setSinkId at module
load and returns null on unsupported platforms (notably iOS Safari).
Split into outer gate + inner body to keep Rules of Hooks intact;
VoicePanel's space-y-5 collapses cleanly with no visible hole.
VideoSection 'Stop preview' CTA gets responsive sizing (mobile:
always-visible 44 px tap target; desktop: original hover-reveal pill via
md: prefixes). The preview <video> gains autoPlay so iOS Safari starts
the stream when srcObject is assigned — manual videoEl.play() after an
awaited getUserMedia loses the user-gesture context on iOS and was
silently rejected by .catch(()=>{}), causing the black-preview bug.
voice.md updated for the iOS hide-when-unsupported policy and the
touch-close contract on device pickers.
This commit is contained in:
@@ -7,6 +7,11 @@ import { MessageList } from '../chat/MessageList';
|
||||
import { MessageInput } from '../chat/MessageInput';
|
||||
import { TypingIndicator } from '../chat/TypingIndicator';
|
||||
import { TransferIndicator } from './TransferIndicator';
|
||||
import { parseFederatedUsername } from '../../utils/identity';
|
||||
import { useCanonicalUserView } from '../../utils/userViewLookup';
|
||||
import type { User } from '@backspace/shared';
|
||||
|
||||
const FALLBACK_USER = { id: '', username: '', createdAt: 0, isAdmin: false, replicatedInstances: [] } as unknown as User;
|
||||
|
||||
interface MobileChatScreenProps {
|
||||
params?: Record<string, string>;
|
||||
@@ -32,16 +37,31 @@ export function MobileChatScreen({ params }: MobileChatScreenProps) {
|
||||
loadMessages(channelId);
|
||||
}, [channelId, setCurrentChannel, loadMessages]);
|
||||
|
||||
// Resolve the "main other member" of a 1:1 DM up-front so we can route it
|
||||
// through useCanonicalUserView (hook, must run unconditionally). Group DMs
|
||||
// don't get the cache treatment in the header — the comma-joined title falls
|
||||
// back to per-member parseFederatedUsername normalization, matching the
|
||||
// pattern in MobileDmsScreen.
|
||||
const dm = isDm && channelId ? dmChannels.find(d => d.id === channelId) : undefined;
|
||||
const otherMembers = dm ? dm.members.filter(m => m.id !== authUser?.id) : [];
|
||||
const isGroup = !!dm?.ownerId;
|
||||
const rawMainOther = !isGroup ? otherMembers[0] : undefined;
|
||||
const canonicalMainOther = useCanonicalUserView((rawMainOther as unknown as User) ?? FALLBACK_USER);
|
||||
|
||||
// Resolve channel/DM name
|
||||
let channelName = 'Channel';
|
||||
if (isDm && channelId) {
|
||||
const dm = dmChannels.find(d => d.id === channelId);
|
||||
if (dm) {
|
||||
const otherMembers = dm.members.filter(m => m.id !== authUser?.id);
|
||||
const isGroup = !!dm.ownerId;
|
||||
channelName = isGroup
|
||||
? otherMembers.map(m => m.displayName ?? m.username).join(', ')
|
||||
: otherMembers[0]?.displayName ?? otherMembers[0]?.username ?? 'Direct Message';
|
||||
if (isDm && dm) {
|
||||
if (isGroup) {
|
||||
channelName = otherMembers
|
||||
.map(m => m.displayName ?? parseFederatedUsername(m.username).baseName)
|
||||
.join(', ');
|
||||
} else if (rawMainOther) {
|
||||
channelName =
|
||||
canonicalMainOther.displayName ??
|
||||
parseFederatedUsername(canonicalMainOther.username).baseName ??
|
||||
'Direct Message';
|
||||
} else {
|
||||
channelName = 'Direct Message';
|
||||
}
|
||||
} else if (!isDm && channelId) {
|
||||
const ch = channels.find(c => c.id === channelId);
|
||||
|
||||
@@ -12,7 +12,7 @@ interface MobileSettingsScreenProps {
|
||||
|
||||
const panelConfig: Record<string, { title: string; component: React.ReactNode }> = {
|
||||
account: { title: 'Account', component: <AccountPanel /> },
|
||||
voice: { title: 'Voice & Audio', component: <VoicePanel /> },
|
||||
voice: { title: 'Voice & Video', component: <VoicePanel /> },
|
||||
privacy: { title: 'Privacy', component: <PrivacyPanel /> },
|
||||
connections: { title: 'Connections', component: <ConnectionsPanel /> },
|
||||
};
|
||||
@@ -76,7 +76,7 @@ export function MobileSettingsScreen({ initialPanel }: MobileSettingsScreenProps
|
||||
// Settings section list
|
||||
const sections = [
|
||||
{ id: 'account', label: 'Account' },
|
||||
{ id: 'voice', label: 'Voice & Audio' },
|
||||
{ id: 'voice', label: 'Voice & Video' },
|
||||
{ id: 'privacy', label: 'Privacy' },
|
||||
{ id: 'connections', label: 'Connections' },
|
||||
...(isAdmin ? [{ id: 'instance', label: 'Instance' }] : []),
|
||||
|
||||
@@ -45,7 +45,7 @@ export function MobileYouScreen() {
|
||||
action: () => pushMobileScreen('settings-connections'),
|
||||
},
|
||||
{
|
||||
label: 'Voice & Audio',
|
||||
label: 'Voice & Video',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 18.75a6 6 0 006-6v-1.5m-6 7.5a6 6 0 01-6-6v-1.5m6 7.5v3.75m-3.75 0h7.5M12 15.75a3 3 0 01-3-3V4.5a3 3 0 116 0v8.25a3 3 0 01-3 3z" />
|
||||
|
||||
Reference in New Issue
Block a user