diff --git a/.changeset/fix-account-signout-confirm.md b/.changeset/fix-account-signout-confirm.md
new file mode 100644
index 000000000..52a09d178
--- /dev/null
+++ b/.changeset/fix-account-signout-confirm.md
@@ -0,0 +1,7 @@
+---
+default: patch
+---
+
+Account switcher: show a confirmation dialog before signing out of an account.
+
+Closes #44
diff --git a/.changeset/fix-autocomplete-selection.md b/.changeset/fix-autocomplete-selection.md
new file mode 100644
index 000000000..ffacbd70c
--- /dev/null
+++ b/.changeset/fix-autocomplete-selection.md
@@ -0,0 +1,7 @@
+---
+default: patch
+---
+
+Autocomplete: pressing Enter now selects the highlighted item instead of sending the message. The first item is highlighted on open and ArrowUp/Down navigate the list while keeping typing focus in the editor. Focus returns to the message editor after completing a mention or emoji.
+
+Closes #35, #69, #79
diff --git a/.changeset/fix-cancel-add-account.md b/.changeset/fix-cancel-add-account.md
new file mode 100644
index 000000000..ef027e098
--- /dev/null
+++ b/.changeset/fix-cancel-add-account.md
@@ -0,0 +1,7 @@
+---
+default: patch
+---
+
+Adding account: show a "Cancel" button next to the "Adding account" label so users can abort the flow.
+
+Closes #66
diff --git a/.changeset/fix-editor-autocapitalize.md b/.changeset/fix-editor-autocapitalize.md
new file mode 100644
index 000000000..b122b49ca
--- /dev/null
+++ b/.changeset/fix-editor-autocapitalize.md
@@ -0,0 +1,7 @@
+---
+default: patch
+---
+
+Message editor: add `autoCapitalize="sentences"` to respect the OS/keyboard capitalisation setting on mobile.
+
+Closes #52
diff --git a/.changeset/fix-favicon-mentions-only.md b/.changeset/fix-favicon-mentions-only.md
new file mode 100644
index 000000000..ba8ce5d93
--- /dev/null
+++ b/.changeset/fix-favicon-mentions-only.md
@@ -0,0 +1,7 @@
+---
+default: patch
+---
+
+Notifications: add "Favicon Dot: Mentions Only" setting — when enabled, the favicon badge only changes for mentions/keywords, not plain unreads.
+
+Closes #168
diff --git a/.changeset/fix-media-volume-persist.md b/.changeset/fix-media-volume-persist.md
new file mode 100644
index 000000000..00790b8d4
--- /dev/null
+++ b/.changeset/fix-media-volume-persist.md
@@ -0,0 +1,7 @@
+---
+default: patch
+---
+
+Video and audio messages: volume level is now persisted across page loads via `localStorage` and shared between all media players.
+
+Closes #120
diff --git a/.changeset/fix-reduced-motion-animation.md b/.changeset/fix-reduced-motion-animation.md
new file mode 100644
index 000000000..804c456cb
--- /dev/null
+++ b/.changeset/fix-reduced-motion-animation.md
@@ -0,0 +1,7 @@
+---
+default: patch
+---
+
+Reduced-motion: add `animation-iteration-count: 1` so spinners stop after one cycle instead of running indefinitely at near-zero speed.
+
+Closes #2
diff --git a/.changeset/fix-server-picker-ios-autofill.md b/.changeset/fix-server-picker-ios-autofill.md
new file mode 100644
index 000000000..52f8df4c2
--- /dev/null
+++ b/.changeset/fix-server-picker-ios-autofill.md
@@ -0,0 +1,7 @@
+---
+default: patch
+---
+
+Server picker: prevent iOS from restoring the old server name while the user is actively editing the input.
+
+Closes #45
diff --git a/.changeset/fix-theme-color-meta.md b/.changeset/fix-theme-color-meta.md
new file mode 100644
index 000000000..ce3160794
--- /dev/null
+++ b/.changeset/fix-theme-color-meta.md
@@ -0,0 +1,7 @@
+---
+default: patch
+---
+
+Browser tab/PWA: use the correct light (`#ffffff`) and dark (`#1b1a21`) theme-color values via `media` attribute on the meta tags.
+
+Closes #103
diff --git a/index.html b/index.html
index 21c34e6d1..62ce1dabb 100644
--- a/index.html
+++ b/index.html
@@ -23,7 +23,8 @@
property="og:description"
content="A Matrix client where you can enjoy the conversation using (not) simple, elegant and secure interface protected by e2ee with the power of open source. And colorful cosmetics. Because yes."
/>
-
+
+
diff --git a/public/manifest.json b/public/manifest.json
index 67deb47a2..696b875ee 100644
--- a/public/manifest.json
+++ b/public/manifest.json
@@ -7,8 +7,8 @@
"display": "standalone",
"orientation": "portrait",
"start_url": "./",
- "background_color": "#fff",
- "theme_color": "#fff",
+ "background_color": "#1b1a21",
+ "theme_color": "#1b1a21",
"icons": [
{
"src": "./public/android/android-chrome-36x36.png",
diff --git a/src/app/components/RenderMessageContent.tsx b/src/app/components/RenderMessageContent.tsx
index f235b7586..b892f7691 100644
--- a/src/app/components/RenderMessageContent.tsx
+++ b/src/app/components/RenderMessageContent.tsx
@@ -28,7 +28,7 @@ import {
VideoContent,
} from './message';
import { UrlPreviewCard, UrlPreviewHolder } from './url-preview';
-import { Image, MediaControl, Video } from './media';
+import { Image, MediaControl, PersistedVolumeVideo } from './media';
import { ImageViewer } from './image-viewer';
import { PdfViewer } from './Pdf-viewer';
import { TextViewer } from './text-viewer';
@@ -323,7 +323,7 @@ function RenderMessageContentInternal({
)
: undefined
}
- renderVideo={(p) => }
+ renderVideo={(p) => }
/>
)}
outlined={outlineAttachment}
diff --git a/src/app/components/editor/Editor.tsx b/src/app/components/editor/Editor.tsx
index 92ef1cb12..27df4cf6c 100644
--- a/src/app/components/editor/Editor.tsx
+++ b/src/app/components/editor/Editor.tsx
@@ -160,6 +160,8 @@ export const CustomEditor = forwardRef(
onKeyDown={handleKeydown}
onKeyUp={onKeyUp}
onPaste={onPaste}
+ // Defer to OS capitalization setting (respects iOS sentence-case toggle).
+ autoCapitalize="sentences"
// keeps focus after pressing send.
onBlur={() => {
if (mobileOrTablet()) ReactEditor.focus(editor);
diff --git a/src/app/components/editor/autocomplete/AutocompleteMenu.css.tsx b/src/app/components/editor/autocomplete/AutocompleteMenu.css.tsx
index a160b6001..fb36858cf 100644
--- a/src/app/components/editor/autocomplete/AutocompleteMenu.css.tsx
+++ b/src/app/components/editor/autocomplete/AutocompleteMenu.css.tsx
@@ -1,4 +1,4 @@
-import { style } from '@vanilla-extract/css';
+import { globalStyle, style } from '@vanilla-extract/css';
import { DefaultReset, color, config } from 'folds';
export const AutocompleteMenuBase = style([
@@ -38,3 +38,11 @@ export const AutocompleteNotice = style([
AutocompleteMenuHeader,
{ color: color.SurfaceVariant.OnContainer },
]);
+
+export const AutocompleteMenuItems = style({});
+
+globalStyle(`${AutocompleteMenuItems} button[data-selected='true']`, {
+ backgroundColor: color.SurfaceVariant.ContainerHover,
+ outline: `2px solid ${color.Primary.Main}`,
+ outlineOffset: -2,
+});
diff --git a/src/app/components/editor/autocomplete/AutocompleteMenu.tsx b/src/app/components/editor/autocomplete/AutocompleteMenu.tsx
index cda3b5e16..0b82001b5 100644
--- a/src/app/components/editor/autocomplete/AutocompleteMenu.tsx
+++ b/src/app/components/editor/autocomplete/AutocompleteMenu.tsx
@@ -1,6 +1,5 @@
-import { ReactNode } from 'react';
+import { ReactNode, useEffect, useLayoutEffect, useRef, useState } from 'react';
import FocusTrap from 'focus-trap-react';
-import { isKeyHotkey } from 'is-hotkey';
import { Header, Menu, Scroll, config } from 'folds';
import { preventScrollWithArrowKey, stopPropagation } from '$utils/keyboard';
@@ -8,6 +7,9 @@ import { useAlive } from '$hooks/useAlive';
import * as css from './AutocompleteMenu.css';
import { BaseAutocompleteMenu } from './BaseAutocompleteMenu';
+export const AUTOCOMPLETE_NAVIGATE_EVENT = 'autocomplete-navigate';
+export type AutocompleteNavigateDetail = { direction: 1 | -1 };
+
type AutocompleteMenuProps = {
requestClose: () => void;
headerContent: ReactNode;
@@ -15,6 +17,9 @@ type AutocompleteMenuProps = {
};
export function AutocompleteMenu({ headerContent, requestClose, children }: AutocompleteMenuProps) {
const alive = useAlive();
+ const itemsRef = useRef(null);
+ const [selectedIndex, setSelectedIndex] = useState(0);
+ const prevButtonCountRef = useRef(-1);
const handleDeactivate = () => {
if (alive()) {
@@ -23,6 +28,41 @@ export function AutocompleteMenu({ headerContent, requestClose, children }: Auto
}
};
+ // Sync data-selected to DOM; reset to index 0 when the item list changes
+ useLayoutEffect(() => {
+ const buttons = Array.from(
+ itemsRef.current?.querySelectorAll('button') ?? []
+ );
+ const count = buttons.length;
+
+ let idx = selectedIndex;
+ if (count !== prevButtonCountRef.current) {
+ prevButtonCountRef.current = count;
+ idx = 0;
+ if (selectedIndex !== 0) setSelectedIndex(0);
+ }
+
+ const safeIdx = Math.max(0, Math.min(idx, count - 1));
+ buttons.forEach((btn, i) => {
+ btn.setAttribute('data-selected', String(i === safeIdx));
+ });
+ }, [selectedIndex]);
+
+ // Listen for navigation events dispatched by the editor key handler
+ useEffect(() => {
+ const container = itemsRef.current?.closest('[data-autocomplete-menu]');
+ if (!container) return undefined;
+ const handler = (e: Event) => {
+ const { direction } = (e as CustomEvent).detail;
+ setSelectedIndex((prev) => {
+ const buttons = itemsRef.current?.querySelectorAll('button') ?? [];
+ return Math.max(0, Math.min(prev + direction, buttons.length - 1));
+ });
+ };
+ container.addEventListener(AUTOCOMPLETE_NAVIGATE_EVENT, handler);
+ return () => container.removeEventListener(AUTOCOMPLETE_NAVIGATE_EVENT, handler);
+ }, []);
+
return (
isKeyHotkey('arrowdown', evt),
- isKeyBackward: (evt: KeyboardEvent) => isKeyHotkey('arrowup', evt),
escapeDeactivates: stopPropagation,
}}
>
@@ -42,7 +80,13 @@ export function AutocompleteMenu({ headerContent, requestClose, children }: Auto
{headerContent}
- {children}
+
+ {children}
+
diff --git a/src/app/components/editor/autocomplete/BaseAutocompleteMenu.tsx b/src/app/components/editor/autocomplete/BaseAutocompleteMenu.tsx
index 3f350a88a..9f1bf316f 100644
--- a/src/app/components/editor/autocomplete/BaseAutocompleteMenu.tsx
+++ b/src/app/components/editor/autocomplete/BaseAutocompleteMenu.tsx
@@ -7,7 +7,9 @@ type BaseAutocompleteMenuProps = {
export function BaseAutocompleteMenu({ children }: BaseAutocompleteMenuProps) {
return (
-
{children}
+
+ {children}
+
);
}
diff --git a/src/app/components/editor/autocomplete/EmoticonAutocomplete.tsx b/src/app/components/editor/autocomplete/EmoticonAutocomplete.tsx
index c8466ae98..4eb4d7ba1 100644
--- a/src/app/components/editor/autocomplete/EmoticonAutocomplete.tsx
+++ b/src/app/components/editor/autocomplete/EmoticonAutocomplete.tsx
@@ -1,5 +1,6 @@
import { KeyboardEvent as ReactKeyboardEvent, useEffect, useMemo } from 'react';
import { Editor } from 'slate';
+import { ReactEditor } from 'slate-react';
import { Box, MenuItem, Text, toRem } from 'folds';
import { Room } from '$types/matrix-sdk';
@@ -91,6 +92,7 @@ export function EmoticonAutocomplete({
const emoticonEl = createEmoticonElement(key, shortcode);
replaceWithElement(editor, query.range, emoticonEl);
moveCursor(editor, true);
+ ReactEditor.focus(editor);
requestClose();
});
diff --git a/src/app/components/editor/autocomplete/RoomMentionAutocomplete.tsx b/src/app/components/editor/autocomplete/RoomMentionAutocomplete.tsx
index e02467122..ff690edce 100644
--- a/src/app/components/editor/autocomplete/RoomMentionAutocomplete.tsx
+++ b/src/app/components/editor/autocomplete/RoomMentionAutocomplete.tsx
@@ -1,5 +1,6 @@
import { KeyboardEvent as ReactKeyboardEvent, useCallback, useEffect } from 'react';
import { Editor } from 'slate';
+import { ReactEditor } from 'slate-react';
import { Avatar, Icon, Icons, MenuItem, Text } from 'folds';
import { JoinRule, MatrixClient } from '$types/matrix-sdk';
import { useAtomValue } from 'jotai';
@@ -115,6 +116,7 @@ export function RoomMentionAutocomplete({
);
replaceWithElement(editor, query.range, mentionEl);
moveCursor(editor, true);
+ ReactEditor.focus(editor);
requestClose();
};
diff --git a/src/app/components/editor/autocomplete/UserMentionAutocomplete.tsx b/src/app/components/editor/autocomplete/UserMentionAutocomplete.tsx
index 919b5c879..e47fe9d07 100644
--- a/src/app/components/editor/autocomplete/UserMentionAutocomplete.tsx
+++ b/src/app/components/editor/autocomplete/UserMentionAutocomplete.tsx
@@ -1,5 +1,6 @@
import { useEffect, KeyboardEvent as ReactKeyboardEvent } from 'react';
import { Editor } from 'slate';
+import { ReactEditor } from 'slate-react';
import { Avatar, Icon, Icons, MenuItem, Text } from 'folds';
import { MatrixClient, Room, RoomMember } from '$types/matrix-sdk';
@@ -113,6 +114,7 @@ export function UserMentionAutocomplete({
);
replaceWithElement(editor, query.range, mentionEl);
moveCursor(editor, true);
+ ReactEditor.focus(editor);
requestClose();
};
diff --git a/src/app/components/media/Video.tsx b/src/app/components/media/Video.tsx
index 307dc9e64..c28efcaa3 100644
--- a/src/app/components/media/Video.tsx
+++ b/src/app/components/media/Video.tsx
@@ -1,4 +1,4 @@
-import { VideoHTMLAttributes, forwardRef } from 'react';
+import { VideoHTMLAttributes, forwardRef, useEffect, useRef } from 'react';
import classNames from 'classnames';
import * as css from './media.css';
@@ -8,3 +8,31 @@ export const Video = forwardRef
)
);
+
+export const MEDIA_VOLUME_KEY = 'mediaVolume';
+
+export function PersistedVolumeVideo({
+ onVolumeChange,
+ ...props
+}: VideoHTMLAttributes) {
+ const innerRef = useRef(null);
+
+ useEffect(() => {
+ const stored = localStorage.getItem(MEDIA_VOLUME_KEY);
+ if (innerRef.current && stored !== null) {
+ const parsed = parseFloat(stored);
+ if (!Number.isNaN(parsed)) innerRef.current.volume = parsed;
+ }
+ }, []);
+
+ return (
+