Skip to content
Merged
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
16 changes: 13 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
"app:build": "tauri build",
"android": "node scripts/android.mjs dev",
"android:build": "node scripts/android.mjs build",
"ios": "tauri ios dev",
"ios:build": "tauri ios build",
"ios": "PATH=\"$PWD/scripts/ios-shim:$PATH\" tauri ios dev",
"ios:build": "PATH=\"$PWD/scripts/ios-shim:$PATH\" tauri ios build",
"test": "vitest run",
"bindings": "node scripts/bindings.mjs",
"test:watch": "vitest",
Expand All @@ -30,6 +30,7 @@
"@phosphor-icons/web": "^2.1.1",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "^2.7.2",
"@tauri-apps/plugin-haptics": "^2.4.2",
"@tauri-apps/plugin-notification": "^2.3.3",
"@tauri-apps/plugin-opener": "^2",
"@tauri-apps/plugin-process": "^2.3.1",
Expand Down
13 changes: 13 additions & 0 deletions scripts/ios-shim/swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/sh
# Stand-in for `swift`, first on PATH for the iOS scripts in package.json.
#
# swift-rs 1.0.7 builds Tauri's Swift package with `swift build`, and SwiftPM
# 6.4's default swift-build backend appends its own macOS `-sdk`/`-target`
# after swift-rs's iOS ones, so the package compiles against AppKit and fails
# on 'OpenGLES/EAGL.h'. The old backend honours the overrides. Drop this once
# swift-rs is fixed upstream.
if [ "$1" = "build" ]; then
shift
exec /usr/bin/swift build --build-system native "$@"
fi
exec /usr/bin/swift "$@"
15 changes: 15 additions & 0 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ tauri-plugin-updater = "2"
# Only for `restart`, which is how an installed update actually takes effect.
tauri-plugin-process = "2"

# Haptics are a phone thing: the plugin has no desktop implementation, and the
# web view on iOS has no `navigator.vibrate` to fall back on. Its capability is
# gated the same way — see `capabilities/mobile.json`.
[target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies]
tauri-plugin-haptics = "2"


# Reaching into the WebView to switch WebRTC on (see `enable_webrtc` in lib.rs).
# The version is pinned to the exact one wry depends on — `with_webview` hands
Expand Down
9 changes: 9 additions & 0 deletions src-tauri/capabilities/mobile.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"$schema": "../gen/schemas/mobile-schema.json",
"identifier": "mobile",
"description": "What only a phone build can ask for.",
"comment": "Separate from `default` because of `platforms`: the haptics plugin is compiled on Android and iOS only (see Cargo.toml), and a capability naming permissions from a plugin that isn't registered fails at startup rather than being ignored.",
"platforms": ["iOS", "android"],
"windows": ["main"],
"permissions": ["haptics:default"]
}
4 changes: 4 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ pub fn run() {
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_process::init());

// The mirror image: haptics only exist on a phone (see Cargo.toml).
#[cfg(mobile)]
let builder = builder.plugin(tauri_plugin_haptics::init());

builder
.manage(AppState::default())
.register_asynchronous_uri_scheme_protocol("horo", handle_media_request)
Expand Down
214 changes: 128 additions & 86 deletions src/components/ChatPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,99 +96,113 @@ export function ChatPane({ room, onBack }: { room: RoomSummary; onBack?: () => v
</button>
)}

<div style={{ position: "relative", flex: "none" }}>
<Avatar
id={room.id}
name={room.name}
mxc={room.avatarUrl}
size={onBack ? 32 : 42}
radius={onBack ? 11 : 15}
/>
{!room.isDirect && (
<ChannelBadge
kind={room.isVideoRoom ? "video" : "text"}
size={18}
ring="var(--ink-950)"
{/* On a phone there's no room for an info button beside the call
button, and the name is where a thumb goes anyway — so the whole
identity block is the way into room info, as in Messages. */}
<Identity
onOpen={onBack ? toggleInfo : undefined}
style={{
display: "flex",
alignItems: "center",
gap: space(onBack ? 10 : 14),
minWidth: 0,
flex: onBack ? 1 : undefined,
}}
>
<div style={{ position: "relative", flex: "none" }}>
<Avatar
id={room.id}
name={room.name}
mxc={room.avatarUrl}
size={onBack ? 32 : 42}
radius={onBack ? 11 : 15}
/>
)}
{room.dmUserId && (
<PresenceDot userId={room.dmUserId} size={13} ring="var(--ink-950)" />
)}
</div>

<div style={{ minWidth: 0, flex: onBack ? 1 : undefined }}>
<div
style={{
display: "flex",
alignItems: "center",
gap: space(8),
minWidth: 0,
}}
>
<span
className="uwu-ellipsis"
style={{
fontFamily: "var(--font-heading)",
fontWeight: 800,
fontSize: headingSize(19),
letterSpacing: "-0.01em",
}}
>
{room.name}
</span>
{/* A lock is the whole message. Spelling out "e2e" beside it said
the same thing twice, and did it in the row that has the least
space to spare. The title carries the detail for anyone who
wants it. */}
{room.isEncrypted && (
<span
title="encrypted end-to-end — only people in this room can read it"
aria-label="end-to-end encrypted"
style={{ display: "flex", flex: "none" }}
>
<Icon name="lock-key" size={14} color="var(--accent-primary)" />
</span>
{!room.isDirect && (
<ChannelBadge
kind={room.isVideoRoom ? "video" : "text"}
size={18}
ring="var(--ink-950)"
/>
)}
{room.dmUserId && (
<PresenceDot userId={room.dmUserId} size={13} ring="var(--ink-950)" />
)}
</div>
{/* A DM's subtitle is the person, not the room: a topic and an
alias are things group rooms have. */}
{room.dmUserId ? (
<PresenceLine
userId={room.dmUserId}
style={{
marginTop: 3,
fontSize: textSize(11.5),
// "last seen 3h ago" wraps to three lines in a narrow header
// and doubles its height. `PresenceLine` is a flex row, so
// `text-overflow` would do nothing here — keeping it on one
// line and clipping the overflow is the part that works.
...(onBack
? { whiteSpace: "nowrap" as const, overflow: "hidden", minWidth: 0 }
: {}),
}}
/>
) : (

<div style={{ minWidth: 0, flex: onBack ? 1 : undefined }}>
<div
className="uwu-ellipsis"
style={{
fontSize: textSize(12.5),
color: "var(--text-secondary)",
maxWidth: 520,
display: "flex",
alignItems: "center",
gap: space(8),
minWidth: 0,
}}
>
{room.topic}{" "}
<span
className="uwu-ellipsis"
style={{
fontFamily: "var(--font-mono)",
color: "var(--text-tertiary)",
fontSize: textSize(11.5),
fontFamily: "var(--font-heading)",
fontWeight: 800,
fontSize: headingSize(19),
letterSpacing: "-0.01em",
}}
>
{room.canonicalAlias ?? ""}
{room.name}
</span>
{/* A lock is the whole message. Spelling out "e2e" beside it said
the same thing twice, and did it in the row that has the least
space to spare. The title carries the detail for anyone who
wants it. */}
{room.isEncrypted && (
<span
title="encrypted end-to-end — only people in this room can read it"
aria-label="end-to-end encrypted"
style={{ display: "flex", flex: "none" }}
>
<Icon name="lock-key" size={14} color="var(--accent-primary)" />
</span>
)}
</div>
)}
</div>
{/* A DM's subtitle is the person, not the room: a topic and an
alias are things group rooms have. */}
{room.dmUserId ? (
<PresenceLine
userId={room.dmUserId}
style={{
marginTop: 3,
fontSize: textSize(11.5),
// "last seen 3h ago" wraps to three lines in a narrow header
// and doubles its height. `PresenceLine` is a flex row, so
// `text-overflow` would do nothing here — keeping it on one
// line and clipping the overflow is the part that works.
...(onBack
? { whiteSpace: "nowrap" as const, overflow: "hidden", minWidth: 0 }
: {}),
}}
/>
) : (
<div
className="uwu-ellipsis"
style={{
fontSize: textSize(12.5),
color: "var(--text-secondary)",
maxWidth: 520,
}}
>
{room.topic}{" "}
<span
style={{
fontFamily: "var(--font-mono)",
color: "var(--text-tertiary)",
fontSize: textSize(11.5),
}}
>
{room.canonicalAlias ?? ""}
</span>
</div>
)}
</div>
</Identity>

<div
{...dragOff}
Expand All @@ -213,12 +227,14 @@ export function ChatPane({ room, onBack }: { room: RoomSummary; onBack?: () => v
danger={inThisCall}
onClick={() => void toggleCall()}
/>
<HeaderButton
icon="info"
title="room info"
highlight={showInfo}
onClick={toggleInfo}
/>
{!onBack && (
<HeaderButton
icon="info"
title="room info"
highlight={showInfo}
onClick={toggleInfo}
/>
)}
</div>
</div>

Expand Down Expand Up @@ -390,3 +406,29 @@ export function EmptyPane() {
</div>
);
}

/**
* The avatar and name in the header: a button into room info on a phone, plain
* layout on desktop, where the info button does that job.
*/
function Identity({
onOpen,
style,
children,
}: {
onOpen?: () => void;
style: React.CSSProperties;
children: React.ReactNode;
}) {
if (!onOpen) return <div style={style}>{children}</div>;

return (
<button
onClick={onOpen}
aria-label="room info"
style={{ ...style, padding: 0, font: "inherit", textAlign: "left", cursor: "pointer" }}
>
{children}
</button>
);
}
Loading
Loading