chore(lint): close all typecheck and eslint tech debt to enable husky pre-commit hook with --max-warnings 0
This commit is contained in:
parent
45f5392a92
commit
88d3db2b24
91 changed files with 593 additions and 726 deletions
|
|
@ -60,10 +60,12 @@ module.exports = {
|
||||||
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
||||||
'@typescript-eslint/no-shadow': 'error',
|
'@typescript-eslint/no-shadow': 'error',
|
||||||
|
|
||||||
// Policy: kept as warnings, not errors. The codebase has ~70 long-standing
|
// Policy: kept as `warn` at the rule level so editors / `eslint --fix` /
|
||||||
// `any` casts and ~15 non-null assertions in matrix-js-sdk interop code.
|
// ad-hoc runs surface them as warnings, but `npm run check:eslint` and
|
||||||
// Promoting to error would block builds on existing usage; turning off
|
// `lint-staged` BOTH pass `--max-warnings 0`, so new occurrences block
|
||||||
// would lose signal on new code. Warnings are visible without blocking.
|
// commit. When unavoidable (matrix-js-sdk boundary, generic helpers,
|
||||||
|
// third-party callback shapes), suppress on the line with
|
||||||
|
// `// eslint-disable-next-line` and a one-line justification.
|
||||||
'@typescript-eslint/no-explicit-any': 'warn',
|
'@typescript-eslint/no-explicit-any': 'warn',
|
||||||
'@typescript-eslint/no-non-null-assertion': 'warn',
|
'@typescript-eslint/no-non-null-assertion': 'warn',
|
||||||
},
|
},
|
||||||
|
|
@ -86,6 +88,11 @@ module.exports = {
|
||||||
'no-plusplus': 'off',
|
'no-plusplus': 'off',
|
||||||
'prefer-template': 'off',
|
'prefer-template': 'off',
|
||||||
'no-param-reassign': 'off',
|
'no-param-reassign': 'off',
|
||||||
|
// `for (;;)` form upstream uses for the iter-loops trips eslint
|
||||||
|
// even though it's intentional — keep upstream control flow.
|
||||||
|
'no-constant-condition': 'off',
|
||||||
|
// Diagnostic `console.log` left as-is in vendor copy.
|
||||||
|
'no-console': 'off',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
||||||
5
.husky/pre-commit
Normal file → Executable file
5
.husky/pre-commit
Normal file → Executable file
|
|
@ -1,3 +1,2 @@
|
||||||
# These are commented until we enable lint and typecheck
|
npx tsc -p tsconfig.json --noEmit
|
||||||
# npx tsc -p tsconfig.json --noEmit
|
npx lint-staged
|
||||||
# npx lint-staged
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ npm run typecheck # tsc --noEmit
|
||||||
|
|
||||||
Build: **Vite 5.4** with vanilla-extract, WASM, PWA plugins.
|
Build: **Vite 5.4** with vanilla-extract, WASM, PWA plugins.
|
||||||
|
|
||||||
> **Note:** `.husky/pre-commit` is currently commented out. `npm run check:eslint` is **green** (0 errors, 116 warnings — kept as warn for `no-explicit-any`/`no-non-null-assertion` policy). `npm run typecheck` still has ~32 known errors (residual project bugs after the TS 5.4 + Bundler migration that cleared ~800 module-resolution errors — see `docs/known-tech-debt-lint/`). Use `bash docs/known-tech-debt-lint/diff.sh` to verify your changes added no new typecheck errors, then `npm run build` for the green build check.
|
> **Note:** `.husky/pre-commit` is enabled and runs `tsc --noEmit` + `lint-staged` (which calls `eslint --max-warnings 0` on staged JS/TS files). Both gates are zero: `npm run typecheck` and `npm run check:eslint` are green (0 errors, 0 warnings). Custom Matrix event-types (`AccountDataEvent.Vojo*`, `PoniesRoomEmotes`, `m.bridge`, `m.call.member` etc.) live in [`src/types/matrix/sdkAugmentation.d.ts`](../../src/types/matrix/sdkAugmentation.d.ts) — add new custom types there to keep `mx.getAccountData` / `mx.getStateEvent` calls type-safe.
|
||||||
|
|
||||||
## Source Layout
|
## Source Layout
|
||||||
|
|
||||||
|
|
@ -287,7 +287,7 @@ i18next + `react-i18next`. Translations in `public/locales/{en,ru}/*.json`, orga
|
||||||
- Current vojo work branch: `vojo/dev`
|
- Current vojo work branch: `vojo/dev`
|
||||||
- Semantic-release on `dev` branch
|
- Semantic-release on `dev` branch
|
||||||
- CI: GitHub Actions (build, deploy, docker, netlify)
|
- CI: GitHub Actions (build, deploy, docker, netlify)
|
||||||
- **Husky pre-commit is currently disabled** — `npm run typecheck` and `npm run check:eslint` do not run automatically. `check:eslint` is green; `typecheck` still has ~32 known errors. Use `bash docs/known-tech-debt-lint/diff.sh` to check your changes don't add new typecheck errors. Re-enable husky once typecheck residual is cleared.
|
- **Husky pre-commit runs `tsc --noEmit` + `lint-staged` (`eslint --max-warnings 0`)** — both must be green to commit. `no-explicit-any` and `no-non-null-assertion` policy: kept as `'warn'` in `.eslintrc.cjs` but blocked by `--max-warnings 0`. When introducing one is unavoidable (matrix-js-sdk boundary, generic helper, third-party callback shape), add an inline `// eslint-disable-next-line` with a one-line justification rather than relaxing the rule.
|
||||||
- **Android `versionCode` is monotonic** (commit 8064760, derived from commit count). Don't squash or rebase across release boundaries — Play store rejects downgrades
|
- **Android `versionCode` is monotonic** (commit 8064760, derived from commit count). Don't squash or rebase across release boundaries — Play store rejects downgrades
|
||||||
- **Commit message style** (vojo memory): one sentence ≤25 words; no body; no Co-Authored-By trailer
|
- **Commit message style** (vojo memory): one sentence ≤25 words; no body; no Co-Authored-By trailer
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
# Известный техдолг по линтеру
|
|
||||||
|
|
||||||
Эта папка фиксирует **известное состояние** `npm run typecheck` в репозитории. Build при этом зелёный, prod задеплоен — это исторический технический долг, не блокер. Папка нужна чтобы при любых изменениях кода сравнивать **delta** (только то что мы добавили), не путаясь в предсуществующих ошибках.
|
|
||||||
|
|
||||||
После апгрейда TypeScript 4.9 → 5.4 + `moduleResolution: "Bundler"` (см. историю коммита) основная масса (~803 из 835 предыдущих ошибок) исчезла. Осталось ~32 ошибки уже про реальные баги/несоответствия типов в нашем коде, не про модульное резолвинг. `npm run check:eslint` теперь — обычный зелёный чек (0 ошибок, 116 warnings), отдельный snapshot не нужен.
|
|
||||||
|
|
||||||
## Состав
|
|
||||||
|
|
||||||
| Файл | Что |
|
|
||||||
|---|---|
|
|
||||||
| `typecheck.snapshot.txt` | Полный stdout `npm run typecheck`. **~61 строка, ~32 ошибки.** |
|
|
||||||
| `diff.sh` | Скрипт сравнения: запускает текущий typecheck, сравнивает с snapshot-ом, выдаёт **только delta**. |
|
|
||||||
|
|
||||||
## Как пользоваться
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash docs/known-tech-debt-lint/diff.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
На чистой ветке выводит:
|
|
||||||
```
|
|
||||||
=== typecheck diff vs known-tech-debt snapshot ===
|
|
||||||
no new typecheck errors
|
|
||||||
```
|
|
||||||
|
|
||||||
Если что-то сломал — выводит конкретные новые ошибки в формате `file.tsx(_,_): error TS...` (line/col маска чтобы pure-line-shift не давал phantom NEW + fixed). Реальные позиции — `npm run typecheck` напрямую. Если случайно починил предсуществующий долг — отчитается «(incidentally fixed: N)» к сведению.
|
|
||||||
|
|
||||||
Скрипт смотрит **working tree**, не staged-состояние. Для строгого pre-commit gate сначала apply'нуть свой stage в чистый worktree (`git stash --keep-index` + `bash diff.sh` + `git stash pop`).
|
|
||||||
|
|
||||||
`npm run check:eslint` запускайте напрямую — он зелёный.
|
|
||||||
|
|
||||||
## Что в долге (TL;DR)
|
|
||||||
|
|
||||||
**Typecheck (~32 ошибок):** реальные несоответствия типов. Категории:
|
|
||||||
|
|
||||||
- TS2345 keyof literal-union mismatch (~14): `mx.getAccountData(string)` / `mx.getStateEvent(...)` ждёт `keyof AccountDataEvents` (узкие литеральные типы), у нас передаются `AccountDataEvent.PoniesEmoteRooms`, `'m.call.member'`, `'in.cinny.spaces'` и т.п. — валидные Matrix event-types, но не в SDK-юнионе.
|
|
||||||
- TS2345 i18next signature (~3): `t('Room.members_count', { count: millify(...) })` — `count` хочет `number`, а `millify()` возвращает `string`. На рантайме отображается корректно (в локалях нет plural-вариантов).
|
|
||||||
- TS2345 / TS18048 `Room | undefined` / `Room | null` после `.filter((r) => !!r)` (~6): TS не пропускает truthy-фильтр без type predicate. UserChips.tsx, AddExisting.tsx, Invites.tsx, GlobalPacks.tsx. Runtime безопасно.
|
|
||||||
- TS2345 `IContent` → `RoomMessageEventContent` (1): MessageEditor.tsx — typing gap между общим content и room-message variant.
|
|
||||||
- TS7006 implicit `any` (6): event-handler params (`evt`, `event`, `ev`) в Message.tsx, EventReaders.tsx, UrlPreviewCard.tsx, LiveChip.tsx, MemberGlance.tsx, ReactionViewer.tsx.
|
|
||||||
- TS2540 read-only `sandbox` (1): CallEmbed.ts — `iframe.sandbox = "..."`. Современные DOM types сделали его `DOMTokenList` read-only, но браузеры всё ещё принимают строку.
|
|
||||||
- TS2353 unknown property `endpoint` (1): push.ts — лишнее поле в `setPusher.data`. SDK типы неполные, sygnal/UnifiedPush его читает.
|
|
||||||
- TS2322 `(number | undefined)[]` → `number[]` (1): usePowerLevelTags.ts — то же truthy-filter narrowing.
|
|
||||||
|
|
||||||
Build зелёный, ESLint зелёный. Все 32 оставшихся ошибки — type-strictness без runtime-импакта (truthy-filter narrowing, узкие SDK literal-union'ы, под-типированные event-handler params, слишком строгий DOM types). Это **известный долг**, не блокер. Будущая чистка — отдельный план (создать `docs/plans/typecheck_residual_cleanup.md` когда возьмёмся).
|
|
||||||
|
|
||||||
## Когда обновлять snapshot
|
|
||||||
|
|
||||||
Когда долг будет частично разруливаться отдельной задачей — после её мерджа пересоздать snapshot:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run typecheck > docs/known-tech-debt-lint/typecheck.snapshot.txt 2>&1
|
|
||||||
```
|
|
||||||
|
|
||||||
И обновить TL;DR в этом README.
|
|
||||||
|
|
||||||
Когда typecheck станет зелёным — удалить эту папку целиком и включить husky pre-commit hook.
|
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
# Compare current `npm run typecheck` output to the known tech-debt snapshot.
|
|
||||||
# Emits ONLY new errors introduced relative to the snapshot — does not dump
|
|
||||||
# the full output, so agents can read the result without burning context.
|
|
||||||
#
|
|
||||||
# Comparison is line/col-insensitive: each error's `(L,C):` location is masked
|
|
||||||
# to `(_,_):` before sorting + comm, so a pure line shift (e.g. one added/
|
|
||||||
# removed line above the error) doesn't trigger a phantom NEW + fixed pair.
|
|
||||||
# Re-run `npm run typecheck` to see real positions for any errors flagged here.
|
|
||||||
#
|
|
||||||
# Caveat: this checks the working tree, not the staged worktree. If you stage
|
|
||||||
# a fix but leave it unstaged, or vice versa, the diff reports the working-tree
|
|
||||||
# state. For a strict pre-commit gate, run from a clean stash-apply state.
|
|
||||||
#
|
|
||||||
# `npm run check:eslint` is now a normal green check (0 errors); no snapshot
|
|
||||||
# needed there. Run it directly if you want to see warnings.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# bash docs/known-tech-debt-lint/diff.sh
|
|
||||||
|
|
||||||
set -u
|
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
||||||
BASELINE_DIR="$ROOT/docs/known-tech-debt-lint"
|
|
||||||
TC_BASE="$BASELINE_DIR/typecheck.snapshot.txt"
|
|
||||||
|
|
||||||
# Temp files registered for cleanup on any exit path (success, error, ^C).
|
|
||||||
TMP_FILES=()
|
|
||||||
cleanup() { [ "${#TMP_FILES[@]}" -gt 0 ] && rm -f "${TMP_FILES[@]}"; }
|
|
||||||
trap cleanup EXIT INT TERM
|
|
||||||
|
|
||||||
# Tracks whether any new errors were introduced. Set to 1 by run_typecheck_diff
|
|
||||||
# when delta > 0; script exits with this code at end so CI / runbooks can use
|
|
||||||
# the script as a gate (e.g. `bash diff.sh && echo OK`).
|
|
||||||
NEW_ERRORS=0
|
|
||||||
|
|
||||||
run_typecheck_diff() {
|
|
||||||
echo "=== typecheck diff vs known-tech-debt snapshot ==="
|
|
||||||
local now tc_rc
|
|
||||||
now="$(mktemp)"
|
|
||||||
TMP_FILES+=("$now")
|
|
||||||
( cd "$ROOT" && npm run typecheck ) >"$now" 2>&1
|
|
||||||
tc_rc=$?
|
|
||||||
# Sanity-check: distinguish "tsc ran and reported errors" (rc=2, expected on
|
|
||||||
# this baseline) from "tsc/npm/node failed to run at all" (rc=other, broken
|
|
||||||
# toolchain). Without this guard a rc=127 "tsc: not found" or rc=1 npm error
|
|
||||||
# could produce stdout with no "error TS" lines and the diff would falsely
|
|
||||||
# report "no new errors" / "incidentally fixed: 33".
|
|
||||||
if [ "$tc_rc" -ne 0 ] && [ "$tc_rc" -ne 2 ] && ! grep -q "error TS" "$now"; then
|
|
||||||
echo " ERROR: 'npm run typecheck' did not run cleanly (exit=$tc_rc):"
|
|
||||||
sed 's/^/ /' "$now"
|
|
||||||
NEW_ERRORS=2
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
# Mask `(line,col):` so a pure line shift doesn't change an error's identity.
|
|
||||||
# We compare on the masked form; for NEW lines we display the masked form (the
|
|
||||||
# real position is reproducible by running `npm run typecheck` directly).
|
|
||||||
# `sort` (NOT `sort -u`): we want to preserve cardinality so that two identical
|
|
||||||
# masked errors in the same file aren't collapsed to one — a regression that
|
|
||||||
# adds a duplicate error would otherwise be hidden by the first occurrence.
|
|
||||||
local mask_re='s/\([0-9]+,[0-9]+\):/(_,_):/'
|
|
||||||
local now_masked base_masked
|
|
||||||
now_masked="$(mktemp)"
|
|
||||||
base_masked="$(mktemp)"
|
|
||||||
TMP_FILES+=("$now_masked" "$base_masked")
|
|
||||||
sed -E "$mask_re" "$now" | grep -E "error TS" | sort > "$now_masked"
|
|
||||||
sed -E "$mask_re" "$TC_BASE" | grep -E "error TS" | sort > "$base_masked"
|
|
||||||
|
|
||||||
local new
|
|
||||||
new="$(comm -23 "$now_masked" "$base_masked")"
|
|
||||||
if [ -z "$new" ]; then
|
|
||||||
echo " no new typecheck errors"
|
|
||||||
else
|
|
||||||
local count
|
|
||||||
count="$(printf '%s\n' "$new" | wc -l)"
|
|
||||||
echo " NEW errors: $count (line/col masked — run \`npm run typecheck\` for real positions)"
|
|
||||||
printf '%s\n' "$new" | sed 's/^/ /'
|
|
||||||
NEW_ERRORS=1
|
|
||||||
fi
|
|
||||||
local fixed
|
|
||||||
fixed="$(comm -13 "$now_masked" "$base_masked")"
|
|
||||||
if [ -n "$fixed" ]; then
|
|
||||||
local fcount
|
|
||||||
fcount="$(printf '%s\n' "$fixed" | wc -l)"
|
|
||||||
echo " (incidentally fixed: $fcount)"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
run_typecheck_diff
|
|
||||||
|
|
||||||
exit "$NEW_ERRORS"
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
|
|
||||||
> vojo@4.11.1 typecheck
|
|
||||||
> tsc --noEmit
|
|
||||||
|
|
||||||
src/app/components/event-readers/EventReaders.tsx(82,31): error TS7006: Parameter 'event' implicitly has an 'any' type.
|
|
||||||
src/app/components/image-pack-view/RoomImagePack.tsx(47,9): error TS2345: Argument of type 'StateEvent.PoniesRoomEmotes' is not assignable to parameter of type 'keyof StateEvents'.
|
|
||||||
src/app/components/image-pack-view/UserImagePack.tsx(16,31): error TS2345: Argument of type 'AccountDataEvent.PoniesUserEmotes' is not assignable to parameter of type 'keyof AccountDataEvents'.
|
|
||||||
src/app/components/room-card/RoomCard.tsx(259,18): error TS2345: Argument of type '["Explore.members_count", { count: string; }]' is not assignable to parameter of type '[key: string | string[], options: TOptionsBase & $Dictionary & { defaultValue: string; }] | [key: string | string[], defaultValue: string, options?: (TOptionsBase & $Dictionary) | undefined] | [key: ...]'.
|
|
||||||
Type '["Explore.members_count", { count: string; }]' is not assignable to type '[key: "Explore.members_count" | "Explore.members_count"[], options?: (TOptionsBase & $Dictionary) | undefined]'.
|
|
||||||
Type at position 1 in source is not compatible with type at position 1 in target.
|
|
||||||
Type '{ count: string; }' is not assignable to type 'TOptionsBase & $Dictionary'.
|
|
||||||
Type '{ count: string; }' is not assignable to type 'TOptionsBase'.
|
|
||||||
Types of property 'count' are incompatible.
|
|
||||||
Type 'string' is not assignable to type 'number'.
|
|
||||||
src/app/components/url-preview/UrlPreviewCard.tsx(57,27): error TS7006: Parameter 'evt' implicitly has an 'any' type.
|
|
||||||
src/app/components/user-profile/UserChips.tsx(271,13): error TS18048: 'room' is possibly 'undefined'.
|
|
||||||
src/app/components/user-profile/UserChips.tsx(272,28): error TS2345: Argument of type 'Room | undefined' is not assignable to parameter of type 'Room'.
|
|
||||||
Type 'undefined' is not assignable to type 'Room'.
|
|
||||||
src/app/components/user-profile/UserChips.tsx(275,26): error TS18048: 'room' is possibly 'undefined'.
|
|
||||||
src/app/components/user-profile/UserChips.tsx(276,29): error TS2345: Argument of type 'Room | undefined' is not assignable to parameter of type 'Room'.
|
|
||||||
Type 'undefined' is not assignable to type 'Room'.
|
|
||||||
src/app/components/user-profile/UserChips.tsx(279,25): error TS2345: Argument of type 'Room | undefined' is not assignable to parameter of type 'Room'.
|
|
||||||
Type 'undefined' is not assignable to type 'Room'.
|
|
||||||
src/app/features/add-existing/AddExisting.tsx(168,18): error TS2345: Argument of type '(Room | undefined)[]' is not assignable to parameter of type 'Room[]'.
|
|
||||||
Type 'Room | undefined' is not assignable to type 'Room'.
|
|
||||||
Type 'undefined' is not assignable to type 'Room'.
|
|
||||||
src/app/features/call-status/LiveChip.tsx(90,35): error TS7006: Parameter 'evt' implicitly has an 'any' type.
|
|
||||||
src/app/features/call-status/MemberGlance.tsx(49,23): error TS7006: Parameter 'evt' implicitly has an 'any' type.
|
|
||||||
src/app/features/common-settings/general/RoomJoinRules.tsx(92,52): error TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string'.
|
|
||||||
Type 'undefined' is not assignable to type 'string'.
|
|
||||||
src/app/features/room/message/Message.tsx(860,31): error TS7006: Parameter 'ev' implicitly has an 'any' type.
|
|
||||||
src/app/features/room/message/MessageEditor.tsx(156,39): error TS2345: Argument of type 'IContent' is not assignable to parameter of type 'RoomMessageEventContent'.
|
|
||||||
Type 'IContent' is not assignable to type 'BaseTimelineEvent & Without<(Without<ReplyEvent, NoRelationEvent> & NoRelationEvent) | (Without<NoRelationEvent, ReplyEvent> & ReplyEvent), (Without<...> & RelationEvent) | (Without<...> & ReplacementEvent<...>)> & Without<...> & ReplacementEvent<...> & FileContent'.
|
|
||||||
Property '"body"' is missing in type 'IContent' but required in type 'BaseTimelineEvent'.
|
|
||||||
src/app/features/room/reaction-viewer/ReactionViewer.tsx(135,33): error TS7006: Parameter 'event' implicitly has an 'any' type.
|
|
||||||
src/app/features/settings/developer-tools/DevelopTools.tsx(30,31): error TS2345: Argument of type 'string' is not assignable to parameter of type 'keyof AccountDataEvents'.
|
|
||||||
src/app/features/settings/developer-tools/DevelopTools.tsx(39,54): error TS2345: Argument of type 'string' is not assignable to parameter of type 'keyof AccountDataEvents'.
|
|
||||||
src/app/features/settings/emojis-stickers/GlobalPacks.tsx(161,44): error TS2345: Argument of type '(PackAddress | undefined)[]' is not assignable to parameter of type 'PackAddress[]'.
|
|
||||||
Type 'PackAddress | undefined' is not assignable to type 'PackAddress'.
|
|
||||||
Type 'undefined' is not assignable to type 'PackAddress'.
|
|
||||||
src/app/features/settings/emojis-stickers/GlobalPacks.tsx(164,39): error TS2345: Argument of type '(PackAddress | undefined)[]' is not assignable to parameter of type 'PackAddress[]'.
|
|
||||||
src/app/features/settings/emojis-stickers/GlobalPacks.tsx(311,27): error TS2345: Argument of type 'AccountDataEvent.PoniesEmoteRooms' is not assignable to parameter of type 'keyof AccountDataEvents'.
|
|
||||||
src/app/features/settings/emojis-stickers/GlobalPacks.tsx(328,31): error TS2345: Argument of type 'AccountDataEvent.PoniesEmoteRooms' is not assignable to parameter of type 'keyof AccountDataEvents'.
|
|
||||||
src/app/hooks/useAccountData.ts(7,62): error TS2345: Argument of type 'string' is not assignable to parameter of type 'keyof AccountDataEvents'.
|
|
||||||
src/app/hooks/usePowerLevelTags.ts(14,9): error TS2322: Type '(number | undefined)[]' is not assignable to type 'number[]'.
|
|
||||||
Type 'number | undefined' is not assignable to type 'number'.
|
|
||||||
Type 'undefined' is not assignable to type 'number'.
|
|
||||||
src/app/pages/client/inbox/Invites.tsx(722,45): error TS2345: Argument of type 'Room | null' is not assignable to parameter of type 'Room'.
|
|
||||||
Type 'null' is not assignable to type 'Room'.
|
|
||||||
src/app/pages/client/sidebar/SpaceTabs.tsx(750,27): error TS2345: Argument of type 'AccountDataEvent.VojoSpaces' is not assignable to parameter of type 'keyof AccountDataEvents'.
|
|
||||||
src/app/pages/client/sidebar/SpaceTabs.tsx(797,25): error TS2345: Argument of type 'AccountDataEvent.VojoSpaces' is not assignable to parameter of type 'keyof AccountDataEvents'.
|
|
||||||
src/app/plugins/call/CallEmbed.ts(129,12): error TS2540: Cannot assign to 'sandbox' because it is a read-only property.
|
|
||||||
src/app/plugins/recent-emoji.ts(45,21): error TS2345: Argument of type 'AccountDataEvent.ElementRecentEmoji' is not assignable to parameter of type 'keyof AccountDataEvents'.
|
|
||||||
src/app/utils/push.ts(160,9): error TS2353: Object literal may only specify known properties, and 'endpoint' does not exist in type '{ format?: string | undefined; url?: string | undefined; brand?: string | undefined; }'.
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"lint": "npm run check:eslint && npm run check:prettier",
|
"lint": "npm run check:eslint && npm run check:prettier",
|
||||||
"check:eslint": "eslint src",
|
"check:eslint": "eslint --max-warnings 0 src",
|
||||||
"check:prettier": "prettier --check .",
|
"check:prettier": "prettier --check .",
|
||||||
"fix:prettier": "prettier --write .",
|
"fix:prettier": "prettier --write .",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
"commit": "git-cz"
|
"commit": "git-cz"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{ts,tsx,js,jsx,mjs,cjs}": "eslint",
|
"*.{ts,tsx,js,jsx,mjs,cjs}": "eslint --max-warnings 0",
|
||||||
"*": "prettier --ignore-unknown --write"
|
"*": "prettier --ignore-unknown --write"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
|
|
|
||||||
|
|
@ -647,7 +647,8 @@
|
||||||
"no_communities": "No communities found!",
|
"no_communities": "No communities found!",
|
||||||
|
|
||||||
"space_badge": "Space",
|
"space_badge": "Space",
|
||||||
"members_count": "{{count}} Members",
|
"members_count_one": "{{formattedCount}} Member",
|
||||||
|
"members_count_other": "{{formattedCount}} Members",
|
||||||
"join": "Join",
|
"join": "Join",
|
||||||
"joining": "Joining",
|
"joining": "Joining",
|
||||||
"retry": "Retry",
|
"retry": "Retry",
|
||||||
|
|
|
||||||
|
|
@ -661,7 +661,10 @@
|
||||||
"no_communities": "Сообщества не найдены!",
|
"no_communities": "Сообщества не найдены!",
|
||||||
|
|
||||||
"space_badge": "Пространство",
|
"space_badge": "Пространство",
|
||||||
"members_count": "{{count}} участников",
|
"members_count_one": "{{formattedCount}} участник",
|
||||||
|
"members_count_few": "{{formattedCount}} участника",
|
||||||
|
"members_count_many": "{{formattedCount}} участников",
|
||||||
|
"members_count_other": "{{formattedCount}} участника",
|
||||||
"join": "Присоединиться",
|
"join": "Присоединиться",
|
||||||
"joining": "Вступление…",
|
"joining": "Вступление…",
|
||||||
"retry": "Повторить",
|
"retry": "Повторить",
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ export function ActionUIA({ authData, ongoingFlow, action, onCancel }: ActionUIA
|
||||||
>
|
>
|
||||||
{stageToComplete.type === AuthType.Password && (
|
{stageToComplete.type === AuthType.Password && (
|
||||||
<PasswordStage
|
<PasswordStage
|
||||||
userId={mx.getUserId()!}
|
userId={mx.getSafeUserId()}
|
||||||
stageData={stageToComplete}
|
stageData={stageToComplete}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
submitAuthDict={action}
|
submitAuthDict={action}
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ function makeUIAAction<T>(
|
||||||
authData: IAuthData,
|
authData: IAuthData,
|
||||||
performAction: PerformAction<T>,
|
performAction: PerformAction<T>,
|
||||||
resolve: (data: T) => void,
|
resolve: (data: T) => void,
|
||||||
reject: (error?: any) => void
|
reject: (error?: unknown) => void
|
||||||
): UIAAction<T> {
|
): UIAAction<T> {
|
||||||
const action: UIAAction<T> = {
|
const action: UIAAction<T> = {
|
||||||
authData,
|
authData,
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ export const ImageOverlay = as<'div', ImageOverlayProps>(
|
||||||
<Modal
|
<Modal
|
||||||
className={ModalWide}
|
className={ModalWide}
|
||||||
size="500"
|
size="500"
|
||||||
onContextMenu={(evt: any) => evt.stopPropagation()}
|
onContextMenu={(evt: React.MouseEvent<HTMLDivElement>) => evt.stopPropagation()}
|
||||||
>
|
>
|
||||||
{renderViewer({
|
{renderViewer({
|
||||||
src,
|
src,
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,8 @@ export function SecretStorageRecoveryPassphrase({
|
||||||
bits
|
bits
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// matrix-js-sdk wants SecretStorageKeyDescriptionAesV1; our local type is structurally compatible but distinct.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const match = await mx.secretStorage.checkKey(decodedRecoveryKey, keyContent as any);
|
const match = await mx.secretStorage.checkKey(decodedRecoveryKey, keyContent as any);
|
||||||
|
|
||||||
if (!match) {
|
if (!match) {
|
||||||
|
|
@ -131,6 +133,8 @@ export function SecretStorageRecoveryKey({
|
||||||
async (recoveryKey) => {
|
async (recoveryKey) => {
|
||||||
const decodedRecoveryKey = decodeRecoveryKey(recoveryKey);
|
const decodedRecoveryKey = decodeRecoveryKey(recoveryKey);
|
||||||
|
|
||||||
|
// matrix-js-sdk wants SecretStorageKeyDescriptionAesV1; our local type is structurally compatible but distinct.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const match = await mx.secretStorage.checkKey(decodedRecoveryKey, keyContent as any);
|
const match = await mx.secretStorage.checkKey(decodedRecoveryKey, keyContent as any);
|
||||||
|
|
||||||
if (!match) {
|
if (!match) {
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,9 @@ export function ServerConfigsLoader({ children }: ServerConfigsLoaderProps) {
|
||||||
try {
|
try {
|
||||||
validatedAuthMetadata = validateAuthMetadata(authMetadata);
|
validatedAuthMetadata = validateAuthMetadata(authMetadata);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
// Auth-metadata parsing failure is non-fatal; the client falls
|
||||||
|
// back to legacy `.well-known` discovery. Surface to dev console.
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
RestrictedAllowType,
|
RestrictedAllowType,
|
||||||
Room,
|
Room,
|
||||||
} from 'matrix-js-sdk';
|
} from 'matrix-js-sdk';
|
||||||
|
import type { StateEvents } from 'matrix-js-sdk';
|
||||||
import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types';
|
import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types';
|
||||||
import { RoomType, StateEvent } from '../../../types/matrix/room';
|
import { RoomType, StateEvent } from '../../../types/matrix/room';
|
||||||
import { getViaServers } from '../../plugins/via-servers';
|
import { getViaServers } from '../../plugins/via-servers';
|
||||||
|
|
@ -17,7 +18,7 @@ export const createRoomCreationContent = (
|
||||||
allowFederation: boolean,
|
allowFederation: boolean,
|
||||||
additionalCreators: string[] | undefined
|
additionalCreators: string[] | undefined
|
||||||
): object => {
|
): object => {
|
||||||
const content: Record<string, any> = {};
|
const content: Record<string, unknown> = {};
|
||||||
if (typeof type === 'string') {
|
if (typeof type === 'string') {
|
||||||
content.type = type;
|
content.type = type;
|
||||||
}
|
}
|
||||||
|
|
@ -152,11 +153,11 @@ export const createRoom = async (mx: MatrixClient, data: CreateRoomData): Promis
|
||||||
if (data.parent) {
|
if (data.parent) {
|
||||||
await mx.sendStateEvent(
|
await mx.sendStateEvent(
|
||||||
data.parent.roomId,
|
data.parent.roomId,
|
||||||
StateEvent.SpaceChild as any,
|
StateEvent.SpaceChild as keyof StateEvents,
|
||||||
{
|
{
|
||||||
auto_join: false,
|
auto_join: false,
|
||||||
suggested: false,
|
suggested: false,
|
||||||
via: [getMxIdServer(mx.getUserId() ?? '') ?? ''],
|
via: [getMxIdServer(mx.getSafeUserId()) ?? ''],
|
||||||
},
|
},
|
||||||
result.room_id
|
result.room_id
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ type MentionAutoCompleteHandler = (roomAliasOrId: string, name: string) => void;
|
||||||
const roomAliasFromQueryText = (mx: MatrixClient, text: string) =>
|
const roomAliasFromQueryText = (mx: MatrixClient, text: string) =>
|
||||||
isRoomAlias(`#${text}`)
|
isRoomAlias(`#${text}`)
|
||||||
? `#${text}`
|
? `#${text}`
|
||||||
: `#${text}${text.endsWith(':') ? '' : ':'}${getMxIdServer(mx.getUserId() ?? '')}`;
|
: `#${text}${text.endsWith(':') ? '' : ':'}${getMxIdServer(mx.getSafeUserId())}`;
|
||||||
|
|
||||||
function UnknownRoomMentionItem({
|
function UnknownRoomMentionItem({
|
||||||
query,
|
query,
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ type MentionAutoCompleteHandler = (userId: string, name: string) => void;
|
||||||
const userIdFromQueryText = (mx: MatrixClient, text: string) =>
|
const userIdFromQueryText = (mx: MatrixClient, text: string) =>
|
||||||
isUserId(`@${text}`)
|
isUserId(`@${text}`)
|
||||||
? `@${text}`
|
? `@${text}`
|
||||||
: `@${text}${text.endsWith(':') ? '' : ':'}${getMxIdServer(mx.getUserId() ?? '')}`;
|
: `@${text}${text.endsWith(':') ? '' : ':'}${getMxIdServer(mx.getSafeUserId())}`;
|
||||||
|
|
||||||
function UnknownMentionItem({
|
function UnknownMentionItem({
|
||||||
userId,
|
userId,
|
||||||
|
|
@ -92,7 +92,7 @@ export function UserMentionAutocomplete({
|
||||||
}: UserMentionAutocompleteProps) {
|
}: UserMentionAutocompleteProps) {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const useAuthentication = useMediaAuthentication();
|
const useAuthentication = useMediaAuthentication();
|
||||||
const roomId: string = room.roomId!;
|
const { roomId } = room;
|
||||||
const roomAliasOrId = room.getCanonicalAlias() || roomId;
|
const roomAliasOrId = room.getCanonicalAlias() || roomId;
|
||||||
const members = useRoomMembers(mx, roomId);
|
const members = useRoomMembers(mx, roomId);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ export const EventReaders = as<'div', EventReadersProps>(
|
||||||
key={readerId}
|
key={readerId}
|
||||||
style={{ padding: `0 ${config.space.S200}` }}
|
style={{ padding: `0 ${config.space.S200}` }}
|
||||||
radii="400"
|
radii="400"
|
||||||
onClick={(event) => {
|
onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
openProfile(
|
openProfile(
|
||||||
room.roomId,
|
room.roomId,
|
||||||
space?.roomId,
|
space?.roomId,
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ type RoomImagePackProps = {
|
||||||
|
|
||||||
export function RoomImagePack({ room, stateKey }: RoomImagePackProps) {
|
export function RoomImagePack({ room, stateKey }: RoomImagePackProps) {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const userId = mx.getUserId()!;
|
const userId = mx.getSafeUserId();
|
||||||
const powerLevels = usePowerLevels(room);
|
const powerLevels = usePowerLevels(room);
|
||||||
const creators = useRoomCreators(room);
|
const creators = useRoomCreators(room);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { useUserImagePack } from '../../hooks/useImagePacks';
|
||||||
export function UserImagePack() {
|
export function UserImagePack() {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
|
|
||||||
const defaultPack = useMemo(() => new ImagePack(mx.getUserId() ?? '', {}, undefined), [mx]);
|
const defaultPack = useMemo(() => new ImagePack(mx.getSafeUserId(), {}, undefined), [mx]);
|
||||||
const imagePack = useUserImagePack();
|
const imagePack = useUserImagePack();
|
||||||
|
|
||||||
const handleUpdate = useCallback(
|
const handleUpdate = useCallback(
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,8 @@ function computeBoxStyle(naturalW?: number, naturalH?: number): React.CSSPropert
|
||||||
// `object-fit: cover` (image) / `contain` (video) on the inner element.
|
// `object-fit: cover` (image) / `contain` (video) on the inner element.
|
||||||
const naturalAspect = naturalW && naturalH ? naturalW / naturalH : NaN;
|
const naturalAspect = naturalW && naturalH ? naturalW / naturalH : NaN;
|
||||||
if (
|
if (
|
||||||
|
!naturalW ||
|
||||||
|
!naturalH ||
|
||||||
!Number.isFinite(naturalAspect) ||
|
!Number.isFinite(naturalAspect) ||
|
||||||
naturalAspect < STREAM_MEDIA_MIN_ASPECT ||
|
naturalAspect < STREAM_MEDIA_MIN_ASPECT ||
|
||||||
naturalAspect > STREAM_MEDIA_MAX_ASPECT
|
naturalAspect > STREAM_MEDIA_MAX_ASPECT
|
||||||
|
|
@ -39,10 +41,10 @@ function computeBoxStyle(naturalW?: number, naturalH?: number): React.CSSPropert
|
||||||
return { width: toRem(STREAM_MEDIA_MAX_DIM), height: toRem(STREAM_MEDIA_MAX_DIM) };
|
return { width: toRem(STREAM_MEDIA_MAX_DIM), height: toRem(STREAM_MEDIA_MAX_DIM) };
|
||||||
}
|
}
|
||||||
if (naturalAspect >= 1) {
|
if (naturalAspect >= 1) {
|
||||||
const w = Math.min(STREAM_MEDIA_MAX_DIM, naturalW!);
|
const w = Math.min(STREAM_MEDIA_MAX_DIM, naturalW);
|
||||||
return { width: toRem(w), height: toRem(w / naturalAspect) };
|
return { width: toRem(w), height: toRem(w / naturalAspect) };
|
||||||
}
|
}
|
||||||
const h = Math.min(STREAM_MEDIA_MAX_DIM, naturalH!);
|
const h = Math.min(STREAM_MEDIA_MAX_DIM, naturalH);
|
||||||
return { width: toRem(h * naturalAspect), height: toRem(h) };
|
return { width: toRem(h * naturalAspect), height: toRem(h) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,7 @@ export function ReadTextFile({ body, mimeType, url, encInfo, renderViewer }: Rea
|
||||||
<Modal
|
<Modal
|
||||||
className={ModalWide}
|
className={ModalWide}
|
||||||
size="500"
|
size="500"
|
||||||
onContextMenu={(evt: any) => evt.stopPropagation()}
|
onContextMenu={(evt: React.MouseEvent<HTMLDivElement>) => evt.stopPropagation()}
|
||||||
>
|
>
|
||||||
{renderViewer({
|
{renderViewer({
|
||||||
name: body,
|
name: body,
|
||||||
|
|
@ -203,7 +203,7 @@ export function ReadPdfFile({ body, mimeType, url, encInfo, renderViewer }: Read
|
||||||
<Modal
|
<Modal
|
||||||
className={ModalWide}
|
className={ModalWide}
|
||||||
size="500"
|
size="500"
|
||||||
onContextMenu={(evt: any) => evt.stopPropagation()}
|
onContextMenu={(evt: React.MouseEvent<HTMLDivElement>) => evt.stopPropagation()}
|
||||||
>
|
>
|
||||||
{renderViewer({
|
{renderViewer({
|
||||||
name: body,
|
name: body,
|
||||||
|
|
|
||||||
|
|
@ -177,7 +177,7 @@ export const ImageContent = as<'div', ImageContentProps>(
|
||||||
<Modal
|
<Modal
|
||||||
className={ModalWide}
|
className={ModalWide}
|
||||||
size="500"
|
size="500"
|
||||||
onContextMenu={(evt: any) => evt.stopPropagation()}
|
onContextMenu={(evt: React.MouseEvent<HTMLDivElement>) => evt.stopPropagation()}
|
||||||
>
|
>
|
||||||
{renderViewer({
|
{renderViewer({
|
||||||
src: srcState.data,
|
src: srcState.data,
|
||||||
|
|
@ -214,10 +214,7 @@ export const ImageContent = as<'div', ImageContentProps>(
|
||||||
)}
|
)}
|
||||||
{srcState.status === AsyncStatus.Success && (
|
{srcState.status === AsyncStatus.Success && (
|
||||||
<Box
|
<Box
|
||||||
className={classNames(
|
className={classNames(css.AbsoluteContainer, blurred ? css.Blur : css.ImageClickable)}
|
||||||
css.AbsoluteContainer,
|
|
||||||
blurred ? css.Blur : css.ImageClickable
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{renderImage({
|
{renderImage({
|
||||||
alt: body,
|
alt: body,
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,7 @@ import classNames from 'classnames';
|
||||||
import { ContainerColor } from '../../styles/ContainerColor.css';
|
import { ContainerColor } from '../../styles/ContainerColor.css';
|
||||||
import * as css from './style.css';
|
import * as css from './style.css';
|
||||||
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
||||||
import {
|
import { SIDEBAR_WIDTH_MIN, clampSidebarWidth, sidebarWidthAtom } from '../../state/sidebarWidth';
|
||||||
SIDEBAR_WIDTH_MIN,
|
|
||||||
clampSidebarWidth,
|
|
||||||
sidebarWidthAtom,
|
|
||||||
} from '../../state/sidebarWidth';
|
|
||||||
import {
|
import {
|
||||||
VOJO_HORSESHOE_VOID_COLOR,
|
VOJO_HORSESHOE_VOID_COLOR,
|
||||||
VOJO_HORSESHOE_GAP_PX,
|
VOJO_HORSESHOE_GAP_PX,
|
||||||
|
|
@ -84,10 +80,7 @@ export function PageRoot({ nav, children }: PageRootProps) {
|
||||||
apparent colour unchanged for routes whose content has no
|
apparent colour unchanged for routes whose content has no
|
||||||
opaque bg of its own (e.g. ChannelsLanding) — without it
|
opaque bg of its own (e.g. ChannelsLanding) — without it
|
||||||
the outer void would bleed through. */}
|
the outer void would bleed through. */}
|
||||||
<Box
|
<Box grow="Yes" style={{ minWidth: 0, backgroundColor: VOJO_HORSESHOE_VOID_COLOR }}>
|
||||||
grow="Yes"
|
|
||||||
style={{ minWidth: 0, backgroundColor: VOJO_HORSESHOE_VOID_COLOR }}
|
|
||||||
>
|
|
||||||
<Box
|
<Box
|
||||||
grow="Yes"
|
grow="Yes"
|
||||||
className={ContainerColor({ variant: 'Background' })}
|
className={ContainerColor({ variant: 'Background' })}
|
||||||
|
|
@ -147,6 +140,9 @@ export function PageNav({
|
||||||
const horseshoe = useHorseshoeEnabled();
|
const horseshoe = useHorseshoeEnabled();
|
||||||
|
|
||||||
if (resizable && !isMobile) {
|
if (resizable && !isMobile) {
|
||||||
|
// `ResizablePageNav` is a function declaration (hoisted) below — the
|
||||||
|
// forward reference is safe at runtime.
|
||||||
|
// eslint-disable-next-line no-use-before-define
|
||||||
return <ResizablePageNav>{children}</ResizablePageNav>;
|
return <ResizablePageNav>{children}</ResizablePageNav>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -160,9 +156,7 @@ export function PageNav({
|
||||||
// we can override the default `Background.Container` without
|
// we can override the default `Background.Container` without
|
||||||
// touching the recipe.
|
// touching the recipe.
|
||||||
const surfaceStyle =
|
const surfaceStyle =
|
||||||
surface === 'surfaceVariant'
|
surface === 'surfaceVariant' ? { backgroundColor: color.SurfaceVariant.Container } : undefined;
|
||||||
? { backgroundColor: color.SurfaceVariant.Container }
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
|
@ -199,9 +193,7 @@ function ResizablePageNav({ children }: { children: ReactNode }) {
|
||||||
const handleRef = useRef<HTMLDivElement>(null);
|
const handleRef = useRef<HTMLDivElement>(null);
|
||||||
const horseshoe = useHorseshoeEnabled();
|
const horseshoe = useHorseshoeEnabled();
|
||||||
const [savedWidth, setSavedWidth] = useAtom(sidebarWidthAtom);
|
const [savedWidth, setSavedWidth] = useAtom(sidebarWidthAtom);
|
||||||
const [vw, setVw] = useState<number>(
|
const [vw, setVw] = useState<number>(typeof window !== 'undefined' ? window.innerWidth : 1280);
|
||||||
typeof window !== 'undefined' ? window.innerWidth : 1280
|
|
||||||
);
|
|
||||||
const [dragging, setDragging] = useState(false);
|
const [dragging, setDragging] = useState(false);
|
||||||
// Live width during a drag — kept in component state so we don't write to
|
// Live width during a drag — kept in component state so we don't write to
|
||||||
// the localStorage-backed atom on every pointermove (hundreds of sync disk
|
// the localStorage-backed atom on every pointermove (hundreds of sync disk
|
||||||
|
|
@ -322,6 +314,11 @@ function ResizablePageNav({ children }: { children: ReactNode }) {
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
{canResize && (
|
{canResize && (
|
||||||
|
// Canonical WAI-ARIA window-splitter pattern: focusable separator
|
||||||
|
// with aria-orientation and current/min/max values. The strict
|
||||||
|
// role-supports-aria-props lookup table doesn't model the splitter
|
||||||
|
// sub-pattern, but assistive tech does.
|
||||||
|
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/role-supports-aria-props, jsx-a11y/no-noninteractive-tabindex
|
||||||
<div
|
<div
|
||||||
ref={handleRef}
|
ref={handleRef}
|
||||||
role="separator"
|
role="separator"
|
||||||
|
|
@ -330,6 +327,7 @@ function ResizablePageNav({ children }: { children: ReactNode }) {
|
||||||
aria-valuemin={SIDEBAR_WIDTH_MIN}
|
aria-valuemin={SIDEBAR_WIDTH_MIN}
|
||||||
aria-valuemax={maxW}
|
aria-valuemax={maxW}
|
||||||
aria-label="Resize sidebar"
|
aria-label="Resize sidebar"
|
||||||
|
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className={css.PageNavResizeHandle}
|
className={css.PageNavResizeHandle}
|
||||||
// On web the page-nav is followed by the horseshoe void gap
|
// On web the page-nav is followed by the horseshoe void gap
|
||||||
|
|
|
||||||
|
|
@ -256,7 +256,10 @@ export const RoomCard = as<'div', RoomCardProps>(
|
||||||
<Box gap="100">
|
<Box gap="100">
|
||||||
<Icon size="50" src={Icons.User} />
|
<Icon size="50" src={Icons.User} />
|
||||||
<Text size="T200">
|
<Text size="T200">
|
||||||
{t('Explore.members_count', { count: millify(joinedMemberCount) })}
|
{t('Explore.members_count', {
|
||||||
|
count: joinedMemberCount,
|
||||||
|
formattedCount: millify(joinedMemberCount),
|
||||||
|
})}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
|
||||||
alt={prev['og:title']}
|
alt={prev['og:title']}
|
||||||
title={prev['og:title']}
|
title={prev['og:title']}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onKeyDown={(evt) => onEnterOrSpace(() => setViewer(true))(evt)}
|
onKeyDown={(evt: React.KeyboardEvent) => onEnterOrSpace(() => setViewer(true))(evt)}
|
||||||
onClick={() => setViewer(true)}
|
onClick={() => setViewer(true)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import React, {
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||||
import { Room } from 'matrix-js-sdk';
|
import { Room } from 'matrix-js-sdk';
|
||||||
|
import type { StateEvents } from 'matrix-js-sdk';
|
||||||
import { stopPropagation } from '../../utils/keyboard';
|
import { stopPropagation } from '../../utils/keyboard';
|
||||||
import { useDirects, useRooms, useSpaces } from '../../state/hooks/roomList';
|
import { useDirects, useRooms, useSpaces } from '../../state/hooks/roomList';
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
|
|
@ -136,7 +137,7 @@ export function AddExistingModal({ parentId, space, requestClose }: AddExistingM
|
||||||
|
|
||||||
await mx.sendStateEvent(
|
await mx.sendStateEvent(
|
||||||
parentId,
|
parentId,
|
||||||
StateEvent.SpaceChild as any,
|
StateEvent.SpaceChild as keyof StateEvents,
|
||||||
{
|
{
|
||||||
auto_join: false,
|
auto_join: false,
|
||||||
suggested: false,
|
suggested: false,
|
||||||
|
|
@ -164,7 +165,9 @@ export function AddExistingModal({ parentId, space, requestClose }: AddExistingM
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleApplyChanges = () => {
|
const handleApplyChanges = () => {
|
||||||
const selectedRooms = selected.map((rId) => getRoom(rId)).filter((room) => room !== undefined);
|
const selectedRooms = selected
|
||||||
|
.map((rId) => getRoom(rId))
|
||||||
|
.filter((room): room is Room => room !== undefined);
|
||||||
applyChanges(selectedRooms).then(() => {
|
applyChanges(selectedRooms).then(() => {
|
||||||
if (alive()) {
|
if (alive()) {
|
||||||
setSelected([]);
|
setSelected([]);
|
||||||
|
|
|
||||||
|
|
@ -264,7 +264,9 @@ export class BotWidgetEmbed {
|
||||||
} catch {
|
} catch {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
void openExternalUrl(url);
|
openExternalUrl(url).catch(() => {
|
||||||
|
/* fire-and-forget: log handled inside openExternalUrl */
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
public constructor(private readonly options: BotWidgetEmbedOptions) {
|
public constructor(private readonly options: BotWidgetEmbedOptions) {
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ export function CallMemberCard({ member }: CallMemberCardProps) {
|
||||||
className={css.CallMemberCard}
|
className={css.CallMemberCard}
|
||||||
variant="SurfaceVariant"
|
variant="SurfaceVariant"
|
||||||
radii="500"
|
radii="500"
|
||||||
onClick={(evt: any) =>
|
onClick={(evt: React.MouseEvent<HTMLButtonElement>) =>
|
||||||
openUserProfile(
|
openUserProfile(
|
||||||
room.roomId,
|
room.roomId,
|
||||||
undefined,
|
undefined,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { useCallback, useRef, useState, FormEventHandler, useEffect } from 'react';
|
import React, { useCallback, useRef, useState, FormEventHandler, useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { MatrixError } from 'matrix-js-sdk';
|
import { MatrixError } from 'matrix-js-sdk';
|
||||||
|
import type { StateEvents, TimelineEvents } from 'matrix-js-sdk';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Chip,
|
Chip,
|
||||||
|
|
@ -53,9 +54,18 @@ export function SendRoomEvent({ type, stateKey, requestClose }: SendRoomEventPro
|
||||||
useCallback(
|
useCallback(
|
||||||
(evtType, evtStateKey, evtContent) => {
|
(evtType, evtStateKey, evtContent) => {
|
||||||
if (typeof evtStateKey === 'string') {
|
if (typeof evtStateKey === 'string') {
|
||||||
return mx.sendStateEvent(room.roomId, evtType as any, evtContent, evtStateKey);
|
return mx.sendStateEvent(
|
||||||
|
room.roomId,
|
||||||
|
evtType as keyof StateEvents,
|
||||||
|
evtContent as StateEvents[keyof StateEvents],
|
||||||
|
evtStateKey
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return mx.sendEvent(room.roomId, evtType as any, evtContent);
|
return mx.sendEvent(
|
||||||
|
room.roomId,
|
||||||
|
evtType as keyof TimelineEvents,
|
||||||
|
evtContent as TimelineEvents[keyof TimelineEvents]
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[mx, room]
|
[mx, room]
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import {
|
||||||
} from 'folds';
|
} from 'folds';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { MatrixError } from 'matrix-js-sdk';
|
import { MatrixError } from 'matrix-js-sdk';
|
||||||
|
import type { StateEvents } from 'matrix-js-sdk';
|
||||||
import { Page, PageHeader } from '../../../components/page';
|
import { Page, PageHeader } from '../../../components/page';
|
||||||
import { SequenceCard } from '../../../components/sequence-card';
|
import { SequenceCard } from '../../../components/sequence-card';
|
||||||
import { TextViewerContent } from '../../../components/text-viewer';
|
import { TextViewerContent } from '../../../components/text-viewer';
|
||||||
|
|
@ -61,7 +62,13 @@ function StateEventEdit({ type, stateKey, content, requestClose }: StateEventEdi
|
||||||
|
|
||||||
const [submitState, submit] = useAsyncCallback<object, MatrixError, [object]>(
|
const [submitState, submit] = useAsyncCallback<object, MatrixError, [object]>(
|
||||||
useCallback(
|
useCallback(
|
||||||
(c) => mx.sendStateEvent(room.roomId, type as any, c, stateKey),
|
(c) =>
|
||||||
|
mx.sendStateEvent(
|
||||||
|
room.roomId,
|
||||||
|
type as keyof StateEvents,
|
||||||
|
c as StateEvents[keyof StateEvents],
|
||||||
|
stateKey
|
||||||
|
),
|
||||||
[mx, room, type, stateKey]
|
[mx, room, type, stateKey]
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import {
|
||||||
} from 'folds';
|
} from 'folds';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { MatrixError } from 'matrix-js-sdk';
|
import { MatrixError } from 'matrix-js-sdk';
|
||||||
|
import type { StateEvents } from 'matrix-js-sdk';
|
||||||
import { SequenceCard } from '../../../components/sequence-card';
|
import { SequenceCard } from '../../../components/sequence-card';
|
||||||
import {
|
import {
|
||||||
ImagePack,
|
ImagePack,
|
||||||
|
|
@ -59,7 +60,12 @@ function CreatePackTile({ packs, roomId }: CreatePackTileProps) {
|
||||||
display_name: name,
|
display_name: name,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
await mx.sendStateEvent(roomId, StateEvent.PoniesRoomEmotes as any, content, stateKey);
|
await mx.sendStateEvent(
|
||||||
|
roomId,
|
||||||
|
StateEvent.PoniesRoomEmotes as keyof StateEvents,
|
||||||
|
content as StateEvents[keyof StateEvents],
|
||||||
|
stateKey
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[mx, roomId]
|
[mx, roomId]
|
||||||
)
|
)
|
||||||
|
|
@ -164,7 +170,12 @@ export function RoomPacks({ onViewPack }: RoomPacksProps) {
|
||||||
for (let i = 0; i < removedPacks.length; i += 1) {
|
for (let i = 0; i < removedPacks.length; i += 1) {
|
||||||
const addr = removedPacks[i];
|
const addr = removedPacks[i];
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
await mx.sendStateEvent(room.roomId, StateEvent.PoniesRoomEmotes as any, {}, addr.stateKey);
|
await mx.sendStateEvent(
|
||||||
|
room.roomId,
|
||||||
|
StateEvent.PoniesRoomEmotes as keyof StateEvents,
|
||||||
|
{} as StateEvents[keyof StateEvents],
|
||||||
|
addr.stateKey
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, [mx, room, removedPacks])
|
}, [mx, room, removedPacks])
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,9 @@ export function RoomPublishedAddresses({ permissions }: RoomPublishedAddressesPr
|
||||||
<SettingTile
|
<SettingTile
|
||||||
title={t('RoomSettings.published_addresses')}
|
title={t('RoomSettings.published_addresses')}
|
||||||
description={
|
description={
|
||||||
|
// Static i18n string from the in-repo locale bundle — only `<b>`
|
||||||
|
// markup, no user-controlled input. Safe.
|
||||||
|
// eslint-disable-next-line react/no-danger
|
||||||
<span dangerouslySetInnerHTML={{ __html: t('RoomSettings.published_addresses_desc') }} />
|
<span dangerouslySetInnerHTML={{ __html: t('RoomSettings.published_addresses_desc') }} />
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import {
|
||||||
} from 'folds';
|
} from 'folds';
|
||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import { MatrixError } from 'matrix-js-sdk';
|
import { MatrixError } from 'matrix-js-sdk';
|
||||||
|
import type { StateEvents } from 'matrix-js-sdk';
|
||||||
import FocusTrap from 'focus-trap-react';
|
import FocusTrap from 'focus-trap-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { SequenceCard } from '../../../components/sequence-card';
|
import { SequenceCard } from '../../../components/sequence-card';
|
||||||
|
|
@ -48,7 +49,7 @@ export function RoomEncryption({ permissions }: RoomEncryptionProps) {
|
||||||
|
|
||||||
const [enableState, enable] = useAsyncCallback(
|
const [enableState, enable] = useAsyncCallback(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
await mx.sendStateEvent(room.roomId, StateEvent.RoomEncryption as any, {
|
await mx.sendStateEvent(room.roomId, StateEvent.RoomEncryption as keyof StateEvents, {
|
||||||
algorithm: ROOM_ENC_ALGO,
|
algorithm: ROOM_ENC_ALGO,
|
||||||
});
|
});
|
||||||
}, [mx, room.roomId])
|
}, [mx, room.roomId])
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
Text,
|
Text,
|
||||||
} from 'folds';
|
} from 'folds';
|
||||||
import { HistoryVisibility, MatrixError } from 'matrix-js-sdk';
|
import { HistoryVisibility, MatrixError } from 'matrix-js-sdk';
|
||||||
|
import type { StateEvents } from 'matrix-js-sdk';
|
||||||
import { RoomHistoryVisibilityEventContent } from 'matrix-js-sdk/lib/types';
|
import { RoomHistoryVisibilityEventContent } from 'matrix-js-sdk/lib/types';
|
||||||
import FocusTrap from 'focus-trap-react';
|
import FocusTrap from 'focus-trap-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
@ -80,7 +81,11 @@ export function RoomHistoryVisibility({ permissions }: RoomHistoryVisibilityProp
|
||||||
const content: RoomHistoryVisibilityEventContent = {
|
const content: RoomHistoryVisibilityEventContent = {
|
||||||
history_visibility: visibility,
|
history_visibility: visibility,
|
||||||
};
|
};
|
||||||
await mx.sendStateEvent(room.roomId, StateEvent.RoomHistoryVisibility as any, content);
|
await mx.sendStateEvent(
|
||||||
|
room.roomId,
|
||||||
|
StateEvent.RoomHistoryVisibility as keyof StateEvents,
|
||||||
|
content
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[mx, room.roomId]
|
[mx, room.roomId]
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import React, { useCallback, useMemo } from 'react';
|
||||||
import { color, Text } from 'folds';
|
import { color, Text } from 'folds';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { JoinRule, MatrixError, RestrictedAllowType } from 'matrix-js-sdk';
|
import { JoinRule, MatrixError, RestrictedAllowType } from 'matrix-js-sdk';
|
||||||
|
import type { StateEvents } from 'matrix-js-sdk';
|
||||||
import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types';
|
import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import {
|
import {
|
||||||
|
|
@ -88,7 +89,7 @@ export function RoomJoinRules({ permissions }: RoomJoinRulesProps) {
|
||||||
|
|
||||||
const parents = getStateEvents(room, StateEvent.SpaceParent)
|
const parents = getStateEvents(room, StateEvent.SpaceParent)
|
||||||
.map((event) => event.getStateKey())
|
.map((event) => event.getStateKey())
|
||||||
.filter((parentId) => typeof parentId === 'string')
|
.filter((parentId): parentId is string => typeof parentId === 'string')
|
||||||
.filter((parentId) => roomParents?.has(parentId));
|
.filter((parentId) => roomParents?.has(parentId));
|
||||||
|
|
||||||
if (parents.length === 0 && space && roomParents) {
|
if (parents.length === 0 && space && roomParents) {
|
||||||
|
|
@ -113,7 +114,7 @@ export function RoomJoinRules({ permissions }: RoomJoinRulesProps) {
|
||||||
join_rule: joinRule as JoinRule,
|
join_rule: joinRule as JoinRule,
|
||||||
};
|
};
|
||||||
if (allow.length > 0) c.allow = allow;
|
if (allow.length > 0) c.allow = allow;
|
||||||
await mx.sendStateEvent(room.roomId, StateEvent.RoomJoinRules as any, c);
|
await mx.sendStateEvent(room.roomId, StateEvent.RoomJoinRules as keyof StateEvents, c);
|
||||||
},
|
},
|
||||||
[mx, room, space, subspaces, roomIdToParents]
|
[mx, room, space, subspaces, roomIdToParents]
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import { useTranslation } from 'react-i18next';
|
||||||
import Linkify from 'linkify-react';
|
import Linkify from 'linkify-react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { JoinRule, MatrixError } from 'matrix-js-sdk';
|
import { JoinRule, MatrixError } from 'matrix-js-sdk';
|
||||||
|
import type { StateEvents } from 'matrix-js-sdk';
|
||||||
import { SequenceCard } from '../../../components/sequence-card';
|
import { SequenceCard } from '../../../components/sequence-card';
|
||||||
import { SequenceCardStyle } from '../../room-settings/styles.css';
|
import { SequenceCardStyle } from '../../room-settings/styles.css';
|
||||||
import { useRoom } from '../../../hooks/useRoom';
|
import { useRoom } from '../../../hooks/useRoom';
|
||||||
|
|
@ -94,15 +95,19 @@ export function RoomProfileEdit({
|
||||||
useCallback(
|
useCallback(
|
||||||
async (roomAvatarMxc?: string | null, roomName?: string, roomTopic?: string) => {
|
async (roomAvatarMxc?: string | null, roomName?: string, roomTopic?: string) => {
|
||||||
if (roomAvatarMxc !== undefined) {
|
if (roomAvatarMxc !== undefined) {
|
||||||
await mx.sendStateEvent(room.roomId, StateEvent.RoomAvatar as any, {
|
await mx.sendStateEvent(room.roomId, StateEvent.RoomAvatar as keyof StateEvents, {
|
||||||
url: roomAvatarMxc,
|
url: roomAvatarMxc,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (roomName !== undefined) {
|
if (roomName !== undefined) {
|
||||||
await mx.sendStateEvent(room.roomId, StateEvent.RoomName as any, { name: roomName });
|
await mx.sendStateEvent(room.roomId, StateEvent.RoomName as keyof StateEvents, {
|
||||||
|
name: roomName,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (roomTopic !== undefined) {
|
if (roomTopic !== undefined) {
|
||||||
await mx.sendStateEvent(room.roomId, StateEvent.RoomTopic as any, { topic: roomTopic });
|
await mx.sendStateEvent(room.roomId, StateEvent.RoomTopic as keyof StateEvents, {
|
||||||
|
topic: roomTopic,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[mx, room.roomId]
|
[mx, room.roomId]
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { Badge, Box, Button, Chip, config, Icon, Icons, Menu, Spinner, Text } from 'folds';
|
import { Badge, Box, Button, Chip, config, Icon, Icons, Menu, Spinner, Text } from 'folds';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import produce from 'immer';
|
import produce from 'immer';
|
||||||
|
import type { StateEvents } from 'matrix-js-sdk';
|
||||||
import { SequenceCard } from '../../../components/sequence-card';
|
import { SequenceCard } from '../../../components/sequence-card';
|
||||||
import { SequenceCardStyle } from '../styles.css';
|
import { SequenceCardStyle } from '../styles.css';
|
||||||
import { SettingTile } from '../../../components/setting-tile';
|
import { SettingTile } from '../../../components/setting-tile';
|
||||||
|
|
@ -87,7 +88,11 @@ export function PermissionGroups({
|
||||||
|
|
||||||
return draftPowerLevels;
|
return draftPowerLevels;
|
||||||
});
|
});
|
||||||
await mx.sendStateEvent(room.roomId, StateEvent.RoomPowerLevels as any, editedPowerLevels);
|
await mx.sendStateEvent(
|
||||||
|
room.roomId,
|
||||||
|
StateEvent.RoomPowerLevels as keyof StateEvents,
|
||||||
|
editedPowerLevels
|
||||||
|
);
|
||||||
}, [mx, room, powerLevels, permissionUpdate, permissionGroups])
|
}, [mx, room, powerLevels, permissionUpdate, permissionGroups])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import {
|
||||||
import { HexColorPicker } from 'react-colorful';
|
import { HexColorPicker } from 'react-colorful';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
|
import type { StateEvents } from 'matrix-js-sdk';
|
||||||
import { Page, PageContent, PageHeader } from '../../../components/page';
|
import { Page, PageContent, PageHeader } from '../../../components/page';
|
||||||
import { IPowerLevels } from '../../../hooks/usePowerLevels';
|
import { IPowerLevels } from '../../../hooks/usePowerLevels';
|
||||||
import { SequenceCard } from '../../../components/sequence-card';
|
import { SequenceCard } from '../../../components/sequence-card';
|
||||||
|
|
@ -336,7 +337,7 @@ export function PowersEditor({ powerLevels, requestClose }: PowersEditorProps) {
|
||||||
deleted.forEach((power) => {
|
deleted.forEach((power) => {
|
||||||
delete content[power];
|
delete content[power];
|
||||||
});
|
});
|
||||||
await mx.sendStateEvent(room.roomId, StateEvent.PowerLevelTags as any, content);
|
await mx.sendStateEvent(room.roomId, StateEvent.PowerLevelTags as keyof StateEvents, content);
|
||||||
}, [mx, room, powerLevelTags, editedPowerTags, deleted])
|
}, [mx, room, powerLevelTags, editedPowerTags, deleted])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import {
|
||||||
Spinner,
|
Spinner,
|
||||||
toRem,
|
toRem,
|
||||||
} from 'folds';
|
} from 'folds';
|
||||||
|
import type { StateEvents } from 'matrix-js-sdk';
|
||||||
import { HierarchyItem } from '../../hooks/useSpaceHierarchy';
|
import { HierarchyItem } from '../../hooks/useSpaceHierarchy';
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
import { MSpaceChildContent, StateEvent } from '../../../types/matrix/room';
|
import { MSpaceChildContent, StateEvent } from '../../../types/matrix/room';
|
||||||
|
|
@ -48,7 +49,12 @@ function SuggestMenuItem({
|
||||||
const [toggleState, handleToggleSuggested] = useAsyncCallback(
|
const [toggleState, handleToggleSuggested] = useAsyncCallback(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
const newContent: MSpaceChildContent = { ...content, suggested: !content.suggested };
|
const newContent: MSpaceChildContent = { ...content, suggested: !content.suggested };
|
||||||
return mx.sendStateEvent(parentId, StateEvent.SpaceChild as any, newContent, roomId);
|
return mx.sendStateEvent(
|
||||||
|
parentId,
|
||||||
|
StateEvent.SpaceChild as keyof StateEvents,
|
||||||
|
newContent,
|
||||||
|
roomId
|
||||||
|
);
|
||||||
}, [mx, parentId, roomId, content])
|
}, [mx, parentId, roomId, content])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -85,7 +91,7 @@ function RemoveMenuItem({
|
||||||
|
|
||||||
const [removeState, handleRemove] = useAsyncCallback(
|
const [removeState, handleRemove] = useAsyncCallback(
|
||||||
useCallback(
|
useCallback(
|
||||||
() => mx.sendStateEvent(parentId, StateEvent.SpaceChild as any, {}, roomId),
|
() => mx.sendStateEvent(parentId, StateEvent.SpaceChild as keyof StateEvents, {}, roomId),
|
||||||
[mx, parentId, roomId]
|
[mx, parentId, roomId]
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { useVirtualizer } from '@tanstack/react-virtual';
|
||||||
import { useAtom, useAtomValue } from 'jotai';
|
import { useAtom, useAtomValue } from 'jotai';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { JoinRule, RestrictedAllowType, Room } from 'matrix-js-sdk';
|
import { JoinRule, RestrictedAllowType, Room } from 'matrix-js-sdk';
|
||||||
|
import type { AccountDataEvents, StateEvents } from 'matrix-js-sdk';
|
||||||
import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types';
|
import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types';
|
||||||
import { IHierarchyRoom } from 'matrix-js-sdk/lib/@types/spaces';
|
import { IHierarchyRoom } from 'matrix-js-sdk/lib/@types/spaces';
|
||||||
import produce from 'immer';
|
import produce from 'immer';
|
||||||
|
|
@ -273,7 +274,7 @@ export function Lobby() {
|
||||||
if (!reorder.item.parentId) return;
|
if (!reorder.item.parentId) return;
|
||||||
await mx.sendStateEvent(
|
await mx.sendStateEvent(
|
||||||
reorder.item.parentId,
|
reorder.item.parentId,
|
||||||
StateEvent.SpaceChild as any,
|
StateEvent.SpaceChild as keyof StateEvents,
|
||||||
{ ...reorder.item.content, order: reorder.orderKey },
|
{ ...reorder.item.content, order: reorder.orderKey },
|
||||||
reorder.item.roomId
|
reorder.item.roomId
|
||||||
);
|
);
|
||||||
|
|
@ -298,7 +299,12 @@ export function Lobby() {
|
||||||
|
|
||||||
// remove from current space
|
// remove from current space
|
||||||
if (item.parentId !== containerParentId) {
|
if (item.parentId !== containerParentId) {
|
||||||
mx.sendStateEvent(item.parentId, StateEvent.SpaceChild as any, {}, item.roomId);
|
mx.sendStateEvent(
|
||||||
|
item.parentId,
|
||||||
|
StateEvent.SpaceChild as keyof StateEvents,
|
||||||
|
{},
|
||||||
|
item.roomId
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|
@ -318,7 +324,7 @@ export function Lobby() {
|
||||||
joinRuleContent.allow?.filter((allowRule) => allowRule.room_id !== item.parentId) ??
|
joinRuleContent.allow?.filter((allowRule) => allowRule.room_id !== item.parentId) ??
|
||||||
[];
|
[];
|
||||||
allow.push({ type: RestrictedAllowType.RoomMembership, room_id: containerParentId });
|
allow.push({ type: RestrictedAllowType.RoomMembership, room_id: containerParentId });
|
||||||
mx.sendStateEvent(itemRoom.roomId, StateEvent.RoomJoinRules as any, {
|
mx.sendStateEvent(itemRoom.roomId, StateEvent.RoomJoinRules as keyof StateEvents, {
|
||||||
...joinRuleContent,
|
...joinRuleContent,
|
||||||
allow,
|
allow,
|
||||||
});
|
});
|
||||||
|
|
@ -360,7 +366,7 @@ export function Lobby() {
|
||||||
await rateLimitedActions(reorders, async (reorder) => {
|
await rateLimitedActions(reorders, async (reorder) => {
|
||||||
await mx.sendStateEvent(
|
await mx.sendStateEvent(
|
||||||
containerParentId,
|
containerParentId,
|
||||||
StateEvent.SpaceChild as any,
|
StateEvent.SpaceChild as keyof StateEvents,
|
||||||
{ ...reorder.item.content, order: reorder.orderKey },
|
{ ...reorder.item.content, order: reorder.orderKey },
|
||||||
reorder.item.roomId
|
reorder.item.roomId
|
||||||
);
|
);
|
||||||
|
|
@ -422,7 +428,10 @@ export function Lobby() {
|
||||||
newItems.push(rId);
|
newItems.push(rId);
|
||||||
}
|
}
|
||||||
const newSpacesContent = makeVojoSpacesContent(mx, newItems);
|
const newSpacesContent = makeVojoSpacesContent(mx, newItems);
|
||||||
mx.setAccountData(AccountDataEvent.VojoSpaces as any, newSpacesContent as any);
|
mx.setAccountData(
|
||||||
|
AccountDataEvent.VojoSpaces as keyof AccountDataEvents,
|
||||||
|
newSpacesContent as AccountDataEvents[keyof AccountDataEvents]
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[mx, sidebarItems, sidebarSpaces]
|
[mx, sidebarItems, sidebarSpaces]
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -333,7 +333,7 @@ function AddSpaceButton({ item }: { item: HierarchyItem }) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateSpace = () => {
|
const handleCreateSpace = () => {
|
||||||
openCreateSpaceModal(item.roomId as any);
|
openCreateSpaceModal(item.roomId);
|
||||||
setCords(undefined);
|
setCords(undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,13 +14,13 @@
|
||||||
|
|
||||||
import React, { MouseEventHandler, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { MouseEventHandler, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { Box, Icon, IconButton, Icons, Spinner, Text } from 'folds';
|
import { Box, Icon, IconButton, Icons, Spinner, Text } from 'folds';
|
||||||
import { PageHeader } from '../../components/page';
|
|
||||||
import { ContainerColor } from '../../styles/ContainerColor.css';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import FileSaver from 'file-saver';
|
import FileSaver from 'file-saver';
|
||||||
import { MatrixEvent, MatrixEventEvent, MsgType, RoomEvent } from 'matrix-js-sdk';
|
import { MatrixEvent, MatrixEventEvent, MsgType, RoomEvent } from 'matrix-js-sdk';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { EncryptedAttachmentInfo } from 'browser-encrypt-attachment';
|
import { EncryptedAttachmentInfo } from 'browser-encrypt-attachment';
|
||||||
|
import { PageHeader } from '../../components/page';
|
||||||
|
import { ContainerColor } from '../../styles/ContainerColor.css';
|
||||||
import { MediaViewerEntry } from '../../state/mediaViewer';
|
import { MediaViewerEntry } from '../../state/mediaViewer';
|
||||||
import { useOpenMediaViewer } from '../../state/hooks/mediaViewer';
|
import { useOpenMediaViewer } from '../../state/hooks/mediaViewer';
|
||||||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||||
|
|
@ -28,11 +28,7 @@ import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||||
import { useRoom } from '../../hooks/useRoom';
|
import { useRoom } from '../../hooks/useRoom';
|
||||||
import { useZoom } from '../../hooks/useZoom';
|
import { useZoom } from '../../hooks/useZoom';
|
||||||
import {
|
import { decryptFile, downloadEncryptedMedia, mxcUrlToHttp } from '../../utils/matrix';
|
||||||
decryptFile,
|
|
||||||
downloadEncryptedMedia,
|
|
||||||
mxcUrlToHttp,
|
|
||||||
} from '../../utils/matrix';
|
|
||||||
import { FALLBACK_MIMETYPE } from '../../utils/mimeTypes';
|
import { FALLBACK_MIMETYPE } from '../../utils/mimeTypes';
|
||||||
import {
|
import {
|
||||||
IImageContent,
|
IImageContent,
|
||||||
|
|
@ -74,7 +70,7 @@ function eventToEntry(roomId: string, ev: MatrixEvent): MediaViewerEntry | null
|
||||||
if (!content) return null;
|
if (!content) return null;
|
||||||
const eventId = ev.getId();
|
const eventId = ev.getId();
|
||||||
if (!eventId) return null;
|
if (!eventId) return null;
|
||||||
const file = content.file;
|
const { file } = content;
|
||||||
const url = file?.url ?? content.url;
|
const url = file?.url ?? content.url;
|
||||||
if (!url) return null;
|
if (!url) return null;
|
||||||
const encInfo: EncryptedAttachmentInfo | undefined = file
|
const encInfo: EncryptedAttachmentInfo | undefined = file
|
||||||
|
|
@ -182,21 +178,18 @@ export function MediaViewerBody({ entry, requestClose }: MediaViewerBodyProps) {
|
||||||
setMaxZoom(Math.min(ZOOM_CEILING_HARD_CAP, computed));
|
setMaxZoom(Math.min(ZOOM_CEILING_HARD_CAP, computed));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const clampPan = useCallback(
|
const clampPan = useCallback((raw: { x: number; y: number }, currentZoom: number) => {
|
||||||
(raw: { x: number; y: number }, currentZoom: number) => {
|
const stage = stageRef.current;
|
||||||
const stage = stageRef.current;
|
const img = imgRef.current;
|
||||||
const img = imgRef.current;
|
if (!stage || !img) return raw;
|
||||||
if (!stage || !img) return raw;
|
const stageRect = stage.getBoundingClientRect();
|
||||||
const stageRect = stage.getBoundingClientRect();
|
const maxX = Math.max(0, (img.offsetWidth * currentZoom - stageRect.width) / 2);
|
||||||
const maxX = Math.max(0, (img.offsetWidth * currentZoom - stageRect.width) / 2);
|
const maxY = Math.max(0, (img.offsetHeight * currentZoom - stageRect.height) / 2);
|
||||||
const maxY = Math.max(0, (img.offsetHeight * currentZoom - stageRect.height) / 2);
|
return {
|
||||||
return {
|
x: Math.max(-maxX, Math.min(maxX, raw.x)),
|
||||||
x: Math.max(-maxX, Math.min(maxX, raw.x)),
|
y: Math.max(-maxY, Math.min(maxY, raw.y)),
|
||||||
y: Math.max(-maxY, Math.min(maxY, raw.y)),
|
};
|
||||||
};
|
}, []);
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Anchor-aware zoom math (image-local point under anchor stays
|
// Anchor-aware zoom math (image-local point under anchor stays
|
||||||
// under the anchor after zoom change) is inlined in the pinch
|
// under the anchor after zoom change) is inlined in the pinch
|
||||||
|
|
@ -279,13 +272,14 @@ export function MediaViewerBody({ entry, requestClose }: MediaViewerBodyProps) {
|
||||||
// `lastBlobUrlRef.current`", the unmount cleanup is
|
// `lastBlobUrlRef.current`", the unmount cleanup is
|
||||||
// unambiguous.
|
// unambiguous.
|
||||||
const lastBlobUrlRef = useRef<string | null>(null);
|
const lastBlobUrlRef = useRef<string | null>(null);
|
||||||
|
const { url: entryUrl, encInfo: entryEncInfo, mimeType: entryMimeType } = entry;
|
||||||
const [srcState, loadSrc] = useAsyncCallback(
|
const [srcState, loadSrc] = useAsyncCallback(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const mediaUrl = mxcUrlToHttp(mx, entry.url, useAuthentication);
|
const mediaUrl = mxcUrlToHttp(mx, entryUrl, useAuthentication);
|
||||||
if (!mediaUrl) throw new Error('Invalid media URL');
|
if (!mediaUrl) throw new Error('Invalid media URL');
|
||||||
if (entry.encInfo) {
|
if (entryEncInfo) {
|
||||||
const fileContent = await downloadEncryptedMedia(mediaUrl, (encBuf) =>
|
const fileContent = await downloadEncryptedMedia(mediaUrl, (encBuf) =>
|
||||||
decryptFile(encBuf, entry.mimeType ?? FALLBACK_MIMETYPE, entry.encInfo!)
|
decryptFile(encBuf, entryMimeType ?? FALLBACK_MIMETYPE, entryEncInfo)
|
||||||
);
|
);
|
||||||
const blob = URL.createObjectURL(fileContent);
|
const blob = URL.createObjectURL(fileContent);
|
||||||
if (lastBlobUrlRef.current && lastBlobUrlRef.current !== blob) {
|
if (lastBlobUrlRef.current && lastBlobUrlRef.current !== blob) {
|
||||||
|
|
@ -295,7 +289,7 @@ export function MediaViewerBody({ entry, requestClose }: MediaViewerBodyProps) {
|
||||||
return blob;
|
return blob;
|
||||||
}
|
}
|
||||||
return mediaUrl;
|
return mediaUrl;
|
||||||
}, [mx, entry.url, entry.encInfo, entry.mimeType, useAuthentication])
|
}, [mx, entryUrl, entryEncInfo, entryMimeType, useAuthentication])
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -375,6 +369,7 @@ export function MediaViewerBody({ entry, requestClose }: MediaViewerBodyProps) {
|
||||||
return entries;
|
return entries;
|
||||||
// `timelineVersion` is the actual refresh trigger; `entry.eventId`
|
// `timelineVersion` is the actual refresh trigger; `entry.eventId`
|
||||||
// is intentionally NOT a dep — stepping doesn't change the set.
|
// is intentionally NOT a dep — stepping doesn't change the set.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [room, timelineVersion]);
|
}, [room, timelineVersion]);
|
||||||
|
|
||||||
const currentIndex = useMemo(
|
const currentIndex = useMemo(
|
||||||
|
|
@ -406,9 +401,7 @@ export function MediaViewerBody({ entry, requestClose }: MediaViewerBodyProps) {
|
||||||
const target = e.target as HTMLElement | null;
|
const target = e.target as HTMLElement | null;
|
||||||
if (
|
if (
|
||||||
target &&
|
target &&
|
||||||
(target.tagName === 'INPUT' ||
|
(target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)
|
||||||
target.tagName === 'TEXTAREA' ||
|
|
||||||
target.isContentEditable)
|
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -622,10 +615,7 @@ export function MediaViewerBody({ entry, requestClose }: MediaViewerBodyProps) {
|
||||||
const pts = Array.from(pointerCacheRef.current.values());
|
const pts = Array.from(pointerCacheRef.current.values());
|
||||||
const dist = distanceBetween(pts[0], pts[1]);
|
const dist = distanceBetween(pts[0], pts[1]);
|
||||||
const ratio = dist / ps.initialDistance;
|
const ratio = dist / ps.initialDistance;
|
||||||
const nextZoom = Math.max(
|
const nextZoom = Math.max(MIN_ZOOM, Math.min(maxZoomRef.current, ps.initialZoom * ratio));
|
||||||
MIN_ZOOM,
|
|
||||||
Math.min(maxZoomRef.current, ps.initialZoom * ratio)
|
|
||||||
);
|
|
||||||
// Anchor-aware: hold `anchorImage*` (image-local point
|
// Anchor-aware: hold `anchorImage*` (image-local point
|
||||||
// captured at pinch start) under the moving midpoint.
|
// captured at pinch start) under the moving midpoint.
|
||||||
const midClientX = (pts[0].x + pts[1].x) / 2;
|
const midClientX = (pts[0].x + pts[1].x) / 2;
|
||||||
|
|
@ -661,8 +651,7 @@ export function MediaViewerBody({ entry, requestClose }: MediaViewerBodyProps) {
|
||||||
}
|
}
|
||||||
if (s.claimed !== 'swipe') return;
|
if (s.claimed !== 'swipe') return;
|
||||||
if (e.cancelable) e.preventDefault();
|
if (e.cancelable) e.preventDefault();
|
||||||
const blocked =
|
const blocked = (dx > 0 && !canPrevRef.current) || (dx < 0 && !canNextRef.current);
|
||||||
(dx > 0 && !canPrevRef.current) || (dx < 0 && !canNextRef.current);
|
|
||||||
setSwipeOffset(blocked ? dx / 3 : dx);
|
setSwipeOffset(blocked ? dx / 3 : dx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -731,10 +720,14 @@ export function MediaViewerBody({ entry, requestClose }: MediaViewerBodyProps) {
|
||||||
const onWheel = (e: WheelEvent) => {
|
const onWheel = (e: WheelEvent) => {
|
||||||
if (entryKindRef.current !== 'image') return;
|
if (entryKindRef.current !== 'image') return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const dyPx =
|
let dyPx: number;
|
||||||
e.deltaMode === 1 ? e.deltaY * 16 // line mode → ~16px / line
|
if (e.deltaMode === 1) {
|
||||||
: e.deltaMode === 2 ? e.deltaY * stageEl.clientHeight // page mode
|
dyPx = e.deltaY * 16; // line mode → ~16px / line
|
||||||
: e.deltaY;
|
} else if (e.deltaMode === 2) {
|
||||||
|
dyPx = e.deltaY * stageEl.clientHeight; // page mode
|
||||||
|
} else {
|
||||||
|
dyPx = e.deltaY;
|
||||||
|
}
|
||||||
const factor = Math.exp(-dyPx * 0.0025);
|
const factor = Math.exp(-dyPx * 0.0025);
|
||||||
const oldZoom = zoomRef.current;
|
const oldZoom = zoomRef.current;
|
||||||
const nextZoomRaw = oldZoom * factor;
|
const nextZoomRaw = oldZoom * factor;
|
||||||
|
|
@ -802,7 +795,8 @@ export function MediaViewerBody({ entry, requestClose }: MediaViewerBodyProps) {
|
||||||
const transform = isReady
|
const transform = isReady
|
||||||
? `translate3d(${swipeOffset + pan.x}px, ${pan.y}px, 0) scale(${zoom})`
|
? `translate3d(${swipeOffset + pan.x}px, ${pan.y}px, 0) scale(${zoom})`
|
||||||
: undefined;
|
: undefined;
|
||||||
const imageTransition = swipeOffset === 0 ? 'transform 200ms cubic-bezier(0.32, 0.72, 0, 1)' : 'none';
|
const imageTransition =
|
||||||
|
swipeOffset === 0 ? 'transform 200ms cubic-bezier(0.32, 0.72, 0, 1)' : 'none';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={css.root}>
|
<div className={css.root}>
|
||||||
|
|
@ -863,9 +857,7 @@ export function MediaViewerBody({ entry, requestClose }: MediaViewerBodyProps) {
|
||||||
>
|
>
|
||||||
<Icon size="100" src={Icons.Download} />
|
<Icon size="100" src={Icons.Download} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
{entry.kind === 'image' && (
|
{entry.kind === 'image' && <div className={css.headerSeparator} aria-hidden="true" />}
|
||||||
<div className={css.headerSeparator} aria-hidden="true" />
|
|
||||||
)}
|
|
||||||
<IconButton
|
<IconButton
|
||||||
variant="Background"
|
variant="Background"
|
||||||
fill="None"
|
fill="None"
|
||||||
|
|
@ -881,6 +873,9 @@ export function MediaViewerBody({ entry, requestClose }: MediaViewerBodyProps) {
|
||||||
|
|
||||||
<div className={css.stage} ref={stageRef}>
|
<div className={css.stage} ref={stageRef}>
|
||||||
{isReady && effectiveSrc && entry.kind === 'image' && (
|
{isReady && effectiveSrc && entry.kind === 'image' && (
|
||||||
|
// `onMouseDown` drives drag-pan when the image is zoomed in.
|
||||||
|
// Keyboard pan is wired separately at the stage level.
|
||||||
|
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
|
||||||
<img
|
<img
|
||||||
ref={imgRef}
|
ref={imgRef}
|
||||||
src={effectiveSrc}
|
src={effectiveSrc}
|
||||||
|
|
@ -965,7 +960,6 @@ export function MediaViewerBody({ entry, requestClose }: MediaViewerBodyProps) {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import { useAtom, useAtomValue } from 'jotai';
|
||||||
import { isKeyHotkey } from 'is-hotkey';
|
import { isKeyHotkey } from 'is-hotkey';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { EventType, IContent, MsgType, RelationType, Room } from 'matrix-js-sdk';
|
import { EventType, IContent, MsgType, RelationType, Room } from 'matrix-js-sdk';
|
||||||
|
import type { RoomMessageEventContent } from 'matrix-js-sdk/lib/types';
|
||||||
import { ReactEditor } from 'slate-react';
|
import { ReactEditor } from 'slate-react';
|
||||||
import { Transforms, Editor } from 'slate';
|
import { Transforms, Editor } from 'slate';
|
||||||
import {
|
import {
|
||||||
|
|
@ -149,12 +150,7 @@ const COMPOSER_PLACEHOLDER_KEYS = [
|
||||||
// no fills.
|
// no fills.
|
||||||
const StreamComposerIcons = {
|
const StreamComposerIcons = {
|
||||||
Plus: () => (
|
Plus: () => (
|
||||||
<path
|
<path d="M12 5v14M5 12h14" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
|
||||||
d="M12 5v14M5 12h14"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="1.8"
|
|
||||||
strokeLinecap="round"
|
|
||||||
/>
|
|
||||||
),
|
),
|
||||||
Smile: () => (
|
Smile: () => (
|
||||||
<>
|
<>
|
||||||
|
|
@ -238,9 +234,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||||
const replyUsernameColor = isOneOnOne ? colorMXID(replyUserID ?? '') : replyPowerColor;
|
const replyUsernameColor = isOneOnOne ? colorMXID(replyUserID ?? '') : replyPowerColor;
|
||||||
|
|
||||||
const [uploadBoard, setUploadBoard] = useState(true);
|
const [uploadBoard, setUploadBoard] = useState(true);
|
||||||
const [selectedFiles, setSelectedFiles] = useAtom(
|
const [selectedFiles, setSelectedFiles] = useAtom(roomIdToUploadItemsAtomFamily(inputDraftKey));
|
||||||
roomIdToUploadItemsAtomFamily(inputDraftKey)
|
|
||||||
);
|
|
||||||
const uploadFamilyObserverAtom = createUploadFamilyObserverAtom(
|
const uploadFamilyObserverAtom = createUploadFamilyObserverAtom(
|
||||||
roomUploadAtomFamily,
|
roomUploadAtomFamily,
|
||||||
selectedFiles.map((f) => f.file)
|
selectedFiles.map((f) => f.file)
|
||||||
|
|
@ -374,7 +368,9 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||||
});
|
});
|
||||||
handleCancelUpload(uploads);
|
handleCancelUpload(uploads);
|
||||||
const contents = fulfilledPromiseSettledResult(await Promise.allSettled(contentsPromises));
|
const contents = fulfilledPromiseSettledResult(await Promise.allSettled(contentsPromises));
|
||||||
contents.forEach((content) => mx.sendMessage(roomId, threadId ?? null, content as any));
|
contents.forEach((content) =>
|
||||||
|
mx.sendMessage(roomId, threadId ?? null, content as RoomMessageEventContent)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const submit = useCallback(() => {
|
const submit = useCallback(() => {
|
||||||
|
|
@ -453,7 +449,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||||
content['m.relates_to'].is_falling_back = false;
|
content['m.relates_to'].is_falling_back = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mx.sendMessage(roomId, threadId ?? null, content as any);
|
mx.sendMessage(roomId, threadId ?? null, content as RoomMessageEventContent);
|
||||||
resetEditor(editor);
|
resetEditor(editor);
|
||||||
resetEditorHistory(editor);
|
resetEditorHistory(editor);
|
||||||
setReplyDraft(undefined);
|
setReplyDraft(undefined);
|
||||||
|
|
@ -634,8 +630,9 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||||
const placeholderKey = useMemo(() => {
|
const placeholderKey = useMemo(() => {
|
||||||
const idx = new Date().getHours() % COMPOSER_PLACEHOLDER_KEYS.length;
|
const idx = new Date().getHours() % COMPOSER_PLACEHOLDER_KEYS.length;
|
||||||
return COMPOSER_PLACEHOLDER_KEYS[idx];
|
return COMPOSER_PLACEHOLDER_KEYS[idx];
|
||||||
// roomId and threadId are intentional re-roll triggers; the
|
// roomId and threadId are intentional re-roll triggers (re-memoize on
|
||||||
// exhaustive-deps lint is happy with them too.
|
// chat/thread navigation), not values read inside the body.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [roomId, threadId]);
|
}, [roomId, threadId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -27,17 +27,7 @@ import { Editor } from 'slate';
|
||||||
import { DEFAULT_EXPIRE_DURATION, SessionMembershipData } from 'matrix-js-sdk/lib/matrixrtc';
|
import { DEFAULT_EXPIRE_DURATION, SessionMembershipData } from 'matrix-js-sdk/lib/matrixrtc';
|
||||||
import to from 'await-to-js';
|
import to from 'await-to-js';
|
||||||
import { useAtomValue, useSetAtom } from 'jotai';
|
import { useAtomValue, useSetAtom } from 'jotai';
|
||||||
import {
|
import { Box, Chip, Icon, Icons, Scroll, Text, as, config, toRem } from 'folds';
|
||||||
Box,
|
|
||||||
Chip,
|
|
||||||
Icon,
|
|
||||||
Icons,
|
|
||||||
Scroll,
|
|
||||||
Text,
|
|
||||||
as,
|
|
||||||
config,
|
|
||||||
toRem,
|
|
||||||
} from 'folds';
|
|
||||||
import { isKeyHotkey } from 'is-hotkey';
|
import { isKeyHotkey } from 'is-hotkey';
|
||||||
import { Opts as LinkifyOpts } from 'linkifyjs';
|
import { Opts as LinkifyOpts } from 'linkifyjs';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
@ -447,7 +437,7 @@ const getEmptyTimeline = () => ({
|
||||||
});
|
});
|
||||||
|
|
||||||
const getRoomUnreadInfo = (room: Room, scrollTo = false) => {
|
const getRoomUnreadInfo = (room: Room, scrollTo = false) => {
|
||||||
const readUptoEventId = room.getEventReadUpTo(room.client.getUserId() ?? '');
|
const readUptoEventId = room.getEventReadUpTo(room.client.getSafeUserId());
|
||||||
if (!readUptoEventId) return undefined;
|
if (!readUptoEventId) return undefined;
|
||||||
const evtTimeline = getEventTimeline(room, readUptoEventId);
|
const evtTimeline = getEventTimeline(room, readUptoEventId);
|
||||||
const latestTimeline = evtTimeline && getFirstLinkedTimeline(evtTimeline, Direction.Forward);
|
const latestTimeline = evtTimeline && getFirstLinkedTimeline(evtTimeline, Direction.Forward);
|
||||||
|
|
@ -794,7 +784,10 @@ export function RoomTimeline({
|
||||||
// Check if the document is in focus (user is actively viewing the app),
|
// Check if the document is in focus (user is actively viewing the app),
|
||||||
// and either there are no unread messages or the latest message is from the current user.
|
// and either there are no unread messages or the latest message is from the current user.
|
||||||
// If either condition is met, trigger the markAsRead function to send a read receipt.
|
// If either condition is met, trigger the markAsRead function to send a read receipt.
|
||||||
requestAnimationFrame(() => markAsRead(mx, mEvt.getRoomId()!, hideActivity));
|
const evtRoomId = mEvt.getRoomId();
|
||||||
|
if (evtRoomId) {
|
||||||
|
requestAnimationFrame(() => markAsRead(mx, evtRoomId, hideActivity));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!document.hasFocus() && !unreadInfo) {
|
if (!document.hasFocus() && !unreadInfo) {
|
||||||
|
|
@ -1308,25 +1301,32 @@ export function RoomTimeline({
|
||||||
// Per-slot ordered list of sessions. Newest session is `last(sessions)`;
|
// Per-slot ordered list of sessions. Newest session is `last(sessions)`;
|
||||||
// earlier entries are closed (count returned to 0).
|
// earlier entries are closed (count returned to 0).
|
||||||
const slotSessions = new Map<string, CallScan[]>();
|
const slotSessions = new Map<string, CallScan[]>();
|
||||||
|
// Hot data-pipeline path: nested for-of with early `continue` is the
|
||||||
|
// clearest expression of the state-machine. Array iteration helpers would
|
||||||
|
// require restructuring the dual-key membership ledger.
|
||||||
|
/* eslint-disable no-restricted-syntax, no-continue */
|
||||||
for (const tl of timeline.linkedTimelines) {
|
for (const tl of timeline.linkedTimelines) {
|
||||||
const events = tl.getEvents();
|
const events = tl.getEvents();
|
||||||
for (const ev of events) {
|
for (const ev of events) {
|
||||||
if (ev.getType() !== StateEvent.GroupCallMemberPrefix) continue;
|
if (ev.getType() !== StateEvent.GroupCallMemberPrefix) continue;
|
||||||
const content = ev.getContent<SessionMembershipData>();
|
const content = ev.getContent<SessionMembershipData>();
|
||||||
const prevContent = ev.getPrevContent() as Partial<SessionMembershipData>;
|
const prevContent = ev.getPrevContent() as Partial<SessionMembershipData>;
|
||||||
const slotId =
|
let slotId: string | null = null;
|
||||||
typeof content.call_id === 'string'
|
if (typeof content.call_id === 'string') {
|
||||||
? content.call_id
|
slotId = content.call_id;
|
||||||
: typeof prevContent.call_id === 'string'
|
} else if (typeof prevContent.call_id === 'string') {
|
||||||
? prevContent.call_id
|
slotId = prevContent.call_id;
|
||||||
: null;
|
}
|
||||||
if (slotId == null) continue;
|
if (slotId == null) continue;
|
||||||
|
// `anchorEventId` is the React key for the merged call bubble; an
|
||||||
|
// empty fallback would collide across multiple eventless rows.
|
||||||
|
const evId = ev.getId();
|
||||||
|
if (!evId) continue;
|
||||||
const ts = ev.getTs();
|
const ts = ev.getTs();
|
||||||
const isJoin = !!content.application;
|
const isJoin = !!content.application;
|
||||||
const wasPreviouslyJoined = !!prevContent.application;
|
const wasPreviouslyJoined = !!prevContent.application;
|
||||||
const sender = ev.getSender() ?? '';
|
const sender = ev.getSender() ?? '';
|
||||||
const stateKey = ev.getStateKey() ?? '';
|
const stateKey = ev.getStateKey() ?? '';
|
||||||
const evId = ev.getId() ?? '';
|
|
||||||
|
|
||||||
let sessions = slotSessions.get(slotId);
|
let sessions = slotSessions.get(slotId);
|
||||||
if (!sessions) {
|
if (!sessions) {
|
||||||
|
|
@ -1407,6 +1407,7 @@ export function RoomTimeline({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* eslint-enable no-restricted-syntax, no-continue */
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
// Consecutive unsuccessful sessions from the same caller within this
|
// Consecutive unsuccessful sessions from the same caller within this
|
||||||
// window collapse into one bubble (WhatsApp/iOS Recents-style anti-spam).
|
// window collapse into one bubble (WhatsApp/iOS Recents-style anti-spam).
|
||||||
|
|
@ -1432,13 +1433,15 @@ export function RoomTimeline({
|
||||||
lastEndTs: number;
|
lastEndTs: number;
|
||||||
};
|
};
|
||||||
const anchors = new Map<string, CallAggregate>();
|
const anchors = new Map<string, CallAggregate>();
|
||||||
|
// Second pass: collapse same-sender unanswered scans into merge units.
|
||||||
|
// Same justification as the upstream loop — pipeline expression with
|
||||||
|
// early `continue` is clearer than reduce+filter chains.
|
||||||
|
/* eslint-disable no-restricted-syntax, no-continue */
|
||||||
for (const sessions of slotSessions.values()) {
|
for (const sessions of slotSessions.values()) {
|
||||||
const units: DisplayUnit[] = [];
|
const units: DisplayUnit[] = [];
|
||||||
for (const scan of sessions) {
|
for (const scan of sessions) {
|
||||||
if (!scan.everJoined && scan.participants.size === 0) continue;
|
if (!scan.everJoined && scan.participants.size === 0) continue;
|
||||||
const ongoing = Array.from(scan.joinedAbsoluteExpiries.values()).some(
|
const ongoing = Array.from(scan.joinedAbsoluteExpiries.values()).some((exp) => exp > now);
|
||||||
(exp) => exp > now
|
|
||||||
);
|
|
||||||
const wasAnswered = scan.connectedAt !== null || scan.participants.size >= 2;
|
const wasAnswered = scan.connectedAt !== null || scan.participants.size >= 2;
|
||||||
const display: SessionDisplay = { scan, ongoing, wasAnswered };
|
const display: SessionDisplay = { scan, ongoing, wasAnswered };
|
||||||
const mergeable = !ongoing && !wasAnswered;
|
const mergeable = !ongoing && !wasAnswered;
|
||||||
|
|
@ -1464,8 +1467,8 @@ export function RoomTimeline({
|
||||||
}
|
}
|
||||||
for (const unit of units) {
|
for (const unit of units) {
|
||||||
const { scan, ongoing, wasAnswered } = unit.anchor;
|
const { scan, ongoing, wasAnswered } = unit.anchor;
|
||||||
const conversationStart = wasAnswered ? (scan.connectedAt ?? scan.startTs) : null;
|
const conversationStart = wasAnswered ? scan.connectedAt ?? scan.startTs : null;
|
||||||
const conversationEnd = wasAnswered && !ongoing ? (scan.endedAt ?? scan.endTs) : null;
|
const conversationEnd = wasAnswered && !ongoing ? scan.endedAt ?? scan.endTs : null;
|
||||||
anchors.set(scan.anchorEventId, {
|
anchors.set(scan.anchorEventId, {
|
||||||
callId: scan.slotId,
|
callId: scan.slotId,
|
||||||
startTs: scan.startTs,
|
startTs: scan.startTs,
|
||||||
|
|
@ -1480,6 +1483,7 @@ export function RoomTimeline({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* eslint-enable no-restricted-syntax, no-continue */
|
||||||
return anchors;
|
return anchors;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
@ -1574,12 +1578,10 @@ export function RoomTimeline({
|
||||||
hideThreadReplyAffordance={hideThreadReplyAffordance}
|
hideThreadReplyAffordance={hideThreadReplyAffordance}
|
||||||
hideMainReplyAffordance={hideMainReplyAffordance}
|
hideMainReplyAffordance={hideMainReplyAffordance}
|
||||||
threadSummary={
|
threadSummary={
|
||||||
showThreadSummary ? (
|
showThreadSummary ? <ThreadSummaryCard room={room} rootEvent={mEvent} /> : undefined
|
||||||
<ThreadSummaryCard room={room} rootEvent={mEvent} />
|
|
||||||
) : undefined
|
|
||||||
}
|
}
|
||||||
layout={messageLayout}
|
layout={messageLayout}
|
||||||
channelHeaderInBubble={channelHeaderInBubble}
|
channelHeaderInBubble={channelHeaderInBubble}
|
||||||
>
|
>
|
||||||
{mEvent.isRedacted() ? (
|
{mEvent.isRedacted() ? (
|
||||||
<RedactedContent reason={mEvent.getUnsigned().redacted_because?.content.reason} />
|
<RedactedContent reason={mEvent.getUnsigned().redacted_because?.content.reason} />
|
||||||
|
|
@ -1690,7 +1692,7 @@ export function RoomTimeline({
|
||||||
) : undefined
|
) : undefined
|
||||||
}
|
}
|
||||||
layout={messageLayout}
|
layout={messageLayout}
|
||||||
channelHeaderInBubble={channelHeaderInBubble}
|
channelHeaderInBubble={channelHeaderInBubble}
|
||||||
>
|
>
|
||||||
{(() => {
|
{(() => {
|
||||||
if (mEvent.isRedacted()) return <RedactedContent />;
|
if (mEvent.isRedacted()) return <RedactedContent />;
|
||||||
|
|
@ -1807,12 +1809,10 @@ export function RoomTimeline({
|
||||||
hideThreadReplyAffordance={hideThreadReplyAffordance}
|
hideThreadReplyAffordance={hideThreadReplyAffordance}
|
||||||
hideMainReplyAffordance={hideMainReplyAffordance}
|
hideMainReplyAffordance={hideMainReplyAffordance}
|
||||||
threadSummary={
|
threadSummary={
|
||||||
showThreadSummary ? (
|
showThreadSummary ? <ThreadSummaryCard room={room} rootEvent={mEvent} /> : undefined
|
||||||
<ThreadSummaryCard room={room} rootEvent={mEvent} />
|
|
||||||
) : undefined
|
|
||||||
}
|
}
|
||||||
layout={messageLayout}
|
layout={messageLayout}
|
||||||
channelHeaderInBubble={channelHeaderInBubble}
|
channelHeaderInBubble={channelHeaderInBubble}
|
||||||
>
|
>
|
||||||
{mEvent.isRedacted() ? (
|
{mEvent.isRedacted() ? (
|
||||||
<RedactedContent reason={mEvent.getUnsigned().redacted_because?.content.reason} />
|
<RedactedContent reason={mEvent.getUnsigned().redacted_because?.content.reason} />
|
||||||
|
|
@ -2025,12 +2025,7 @@ export function RoomTimeline({
|
||||||
const senderId = mEvent.getSender() ?? '';
|
const senderId = mEvent.getSender() ?? '';
|
||||||
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
|
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
|
||||||
|
|
||||||
const timeJSX = (
|
const timeJSX = <Time ts={mEvent.getTs()} compact />;
|
||||||
<Time
|
|
||||||
ts={mEvent.getTs()}
|
|
||||||
compact
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Event
|
<Event
|
||||||
|
|
@ -2049,7 +2044,6 @@ export function RoomTimeline({
|
||||||
railStart={streamRailStart}
|
railStart={streamRailStart}
|
||||||
railEnd={streamRailEnd}
|
railEnd={streamRailEnd}
|
||||||
layout={messageLayout}
|
layout={messageLayout}
|
||||||
channelHeaderInBubble={channelHeaderInBubble}
|
|
||||||
iconSrc={Icons.Code}
|
iconSrc={Icons.Code}
|
||||||
content={
|
content={
|
||||||
<Box grow="Yes" direction="Column">
|
<Box grow="Yes" direction="Column">
|
||||||
|
|
@ -2075,12 +2069,7 @@ export function RoomTimeline({
|
||||||
const senderId = mEvent.getSender() ?? '';
|
const senderId = mEvent.getSender() ?? '';
|
||||||
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
|
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
|
||||||
|
|
||||||
const timeJSX = (
|
const timeJSX = <Time ts={mEvent.getTs()} compact />;
|
||||||
<Time
|
|
||||||
ts={mEvent.getTs()}
|
|
||||||
compact
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Event
|
<Event
|
||||||
|
|
@ -2099,7 +2088,6 @@ export function RoomTimeline({
|
||||||
railStart={streamRailStart}
|
railStart={streamRailStart}
|
||||||
railEnd={streamRailEnd}
|
railEnd={streamRailEnd}
|
||||||
layout={messageLayout}
|
layout={messageLayout}
|
||||||
channelHeaderInBubble={channelHeaderInBubble}
|
|
||||||
iconSrc={Icons.Code}
|
iconSrc={Icons.Code}
|
||||||
content={
|
content={
|
||||||
<Box grow="Yes" direction="Column">
|
<Box grow="Yes" direction="Column">
|
||||||
|
|
@ -2280,19 +2268,20 @@ export function RoomTimeline({
|
||||||
// the rail-endpoint scan and the renderer never disagree on visibility.
|
// the rail-endpoint scan and the renderer never disagree on visibility.
|
||||||
const channelsModeHidden = channelsMode && isChannelsModeHidden(room, mEvent, isBridged);
|
const channelsModeHidden = channelsMode && isChannelsModeHidden(room, mEvent, isBridged);
|
||||||
|
|
||||||
const eventJSX = channelsModeHidden || reactionOrEditEvent(mEvent)
|
const eventJSX =
|
||||||
? null
|
channelsModeHidden || reactionOrEditEvent(mEvent)
|
||||||
: renderMatrixEvent(
|
? null
|
||||||
mEvent.getType(),
|
: renderMatrixEvent(
|
||||||
typeof mEvent.getStateKey() === 'string',
|
mEvent.getType(),
|
||||||
mEventId,
|
typeof mEvent.getStateKey() === 'string',
|
||||||
mEvent,
|
mEventId,
|
||||||
item,
|
mEvent,
|
||||||
timelineSet,
|
item,
|
||||||
collapsed,
|
timelineSet,
|
||||||
streamRailStart,
|
collapsed,
|
||||||
streamRailEnd
|
streamRailStart,
|
||||||
);
|
streamRailEnd
|
||||||
|
);
|
||||||
prevEvent = mEvent;
|
prevEvent = mEvent;
|
||||||
isPrevRendered = !!eventJSX;
|
isPrevRendered = !!eventJSX;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,8 @@ export function RoomTombstone({ roomId, body, replacementRoomId }: RoomTombstone
|
||||||
<Text size="T400">{body || 'This room has been replaced and is no longer active.'}</Text>
|
<Text size="T400">{body || 'This room has been replaced and is no longer active.'}</Text>
|
||||||
{joinState.status === AsyncStatus.Error && (
|
{joinState.status === AsyncStatus.Error && (
|
||||||
<Text style={{ color: color.Critical.Main }} size="T200">
|
<Text style={{ color: color.Critical.Main }} size="T200">
|
||||||
{(joinState.error as any)?.message ?? 'Failed to join replacement room!'}
|
{(joinState.error as { message?: string } | undefined)?.message ??
|
||||||
|
'Failed to join replacement room!'}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,7 @@ import { config } from 'folds';
|
||||||
import FocusTrap from 'focus-trap-react';
|
import FocusTrap from 'focus-trap-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { userRoomProfileAtom } from '../../state/userRoomProfile';
|
import { userRoomProfileAtom } from '../../state/userRoomProfile';
|
||||||
import {
|
import { useCloseUserRoomProfile, useOpenUserRoomProfile } from '../../state/hooks/userRoomProfile';
|
||||||
useCloseUserRoomProfile,
|
|
||||||
useOpenUserRoomProfile,
|
|
||||||
} from '../../state/hooks/userRoomProfile';
|
|
||||||
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
||||||
import { useIsOneOnOne, useRoom } from '../../hooks/useRoom';
|
import { useIsOneOnOne, useRoom } from '../../hooks/useRoom';
|
||||||
import { useSpaceOptionally } from '../../hooks/useSpace';
|
import { useSpaceOptionally } from '../../hooks/useSpace';
|
||||||
|
|
@ -95,8 +92,7 @@ const VAUL_EASING = 'cubic-bezier(0.32, 0.72, 0, 1)';
|
||||||
// which is the felt-feel the user signed off on. Release animations
|
// which is the felt-feel the user signed off on. Release animations
|
||||||
// keep the asymmetric Vaul curve in CSS where direct manipulation
|
// keep the asymmetric Vaul curve in CSS where direct manipulation
|
||||||
// is over and the auto-animation can take centre stage.
|
// is over and the auto-animation can take centre stage.
|
||||||
const easeInOutCubic = (t: number): number =>
|
const easeInOutCubic = (t: number): number => (t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2);
|
||||||
t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
|
||||||
|
|
||||||
type RoomViewProfilePanelProps = {
|
type RoomViewProfilePanelProps = {
|
||||||
header: ReactNode;
|
header: ReactNode;
|
||||||
|
|
@ -138,8 +134,7 @@ function MobileProfileHorseshoe({ header, children }: RoomViewProfilePanelProps)
|
||||||
|
|
||||||
const myUserId = mx.getSafeUserId();
|
const myUserId = mx.getSafeUserId();
|
||||||
const peerCandidate = isOneOnOne ? guessDmRoomUserId(room, myUserId) : undefined;
|
const peerCandidate = isOneOnOne ? guessDmRoomUserId(room, myUserId) : undefined;
|
||||||
const headerDragPeer =
|
const headerDragPeer = peerCandidate && peerCandidate !== myUserId ? peerCandidate : undefined;
|
||||||
peerCandidate && peerCandidate !== myUserId ? peerCandidate : undefined;
|
|
||||||
const headerDragEnabled = !!headerDragPeer;
|
const headerDragEnabled = !!headerDragPeer;
|
||||||
|
|
||||||
const [drag, setDrag] = useState<DragState | null>(null);
|
const [drag, setDrag] = useState<DragState | null>(null);
|
||||||
|
|
@ -215,8 +210,7 @@ function MobileProfileHorseshoe({ header, children }: RoomViewProfilePanelProps)
|
||||||
// (very rare — multiple stacked moderation alerts). In the common
|
// (very rare — multiple stacked moderation alerts). In the common
|
||||||
// case the rail is exactly content-sized and there is nothing to
|
// case the rail is exactly content-sized and there is nothing to
|
||||||
// scroll — `overflow: hidden` then prevents any drag-inside-drag.
|
// scroll — `overflow: hidden` then prevents any drag-inside-drag.
|
||||||
const contentOverflows =
|
const contentOverflows = contentNaturalHeight > 0 && contentNaturalHeight > maxRailPx;
|
||||||
contentNaturalHeight > 0 && contentNaturalHeight > maxRailPx;
|
|
||||||
|
|
||||||
// Measure the chat header's natural height (incl. its safe-top padding).
|
// Measure the chat header's natural height (incl. its safe-top padding).
|
||||||
// `scrollHeight` returns the content size even while the outer is
|
// `scrollHeight` returns the content size even while the outer is
|
||||||
|
|
@ -252,9 +246,7 @@ function MobileProfileHorseshoe({ header, children }: RoomViewProfilePanelProps)
|
||||||
if (liveUserId) lastUserIdRef.current = liveUserId;
|
if (liveUserId) lastUserIdRef.current = liveUserId;
|
||||||
const renderUserId = liveUserId ?? lastUserIdRef.current;
|
const renderUserId = liveUserId ?? lastUserIdRef.current;
|
||||||
|
|
||||||
const renderUserAvatarMxc = renderUserId
|
const renderUserAvatarMxc = renderUserId ? getMemberAvatarMxc(room, renderUserId) : undefined;
|
||||||
? getMemberAvatarMxc(room, renderUserId)
|
|
||||||
: undefined;
|
|
||||||
const renderUserAvatarUrl =
|
const renderUserAvatarUrl =
|
||||||
(renderUserAvatarMxc &&
|
(renderUserAvatarMxc &&
|
||||||
mxcUrlToHttp(mx, renderUserAvatarMxc, useAuthentication, 720, 720, 'scale')) ??
|
mxcUrlToHttp(mx, renderUserAvatarMxc, useAuthentication, 720, 720, 'scale')) ??
|
||||||
|
|
@ -444,22 +436,21 @@ function MobileProfileHorseshoe({ header, children }: RoomViewProfilePanelProps)
|
||||||
// The same ramp factor feeds all three properties so they always
|
// The same ramp factor feeds all three properties so they always
|
||||||
// appear and disappear together — no per-element timing skew that
|
// appear and disappear together — no per-element timing skew that
|
||||||
// could create a perceived «blip».
|
// could create a perceived «blip».
|
||||||
const horseshoeRamp = isDragging
|
let horseshoeRamp: number;
|
||||||
? easeInOutCubic(Math.min(1, expandedPx / HORSESHOE_EMERGE_PX))
|
if (isDragging) {
|
||||||
: horseshoeActive
|
horseshoeRamp = easeInOutCubic(Math.min(1, expandedPx / HORSESHOE_EMERGE_PX));
|
||||||
? 1
|
} else if (horseshoeActive) {
|
||||||
: 0;
|
horseshoeRamp = 1;
|
||||||
|
} else {
|
||||||
|
horseshoeRamp = 0;
|
||||||
|
}
|
||||||
|
|
||||||
const silhouetteRadiusPx = horseshoeRamp * css.HORSESHOE_RADIUS_PX;
|
const silhouetteRadiusPx = horseshoeRamp * css.HORSESHOE_RADIUS_PX;
|
||||||
const chatRadiusPx = horseshoeRamp * css.HORSESHOE_RADIUS_PX;
|
const chatRadiusPx = horseshoeRamp * css.HORSESHOE_RADIUS_PX;
|
||||||
const chatGapPx = horseshoeRamp * css.HORSESHOE_GAP_PX;
|
const chatGapPx = horseshoeRamp * css.HORSESHOE_GAP_PX;
|
||||||
|
|
||||||
const panelViewportTransition = isDragging
|
const panelViewportTransition = isDragging ? 'none' : `height ${ANIMATION_MS}ms ${VAUL_EASING}`;
|
||||||
? 'none'
|
const headerViewportTransition = isDragging ? 'none' : `height ${ANIMATION_MS}ms ${VAUL_EASING}`;
|
||||||
: `height ${ANIMATION_MS}ms ${VAUL_EASING}`;
|
|
||||||
const headerViewportTransition = isDragging
|
|
||||||
? 'none'
|
|
||||||
: `height ${ANIMATION_MS}ms ${VAUL_EASING}`;
|
|
||||||
const silhouetteTransition = isDragging
|
const silhouetteTransition = isDragging
|
||||||
? 'none'
|
? 'none'
|
||||||
: `border-bottom-left-radius ${ANIMATION_MS}ms ${VAUL_EASING}, border-bottom-right-radius ${ANIMATION_MS}ms ${VAUL_EASING}`;
|
: `border-bottom-left-radius ${ANIMATION_MS}ms ${VAUL_EASING}, border-bottom-right-radius ${ANIMATION_MS}ms ${VAUL_EASING}`;
|
||||||
|
|
|
||||||
|
|
@ -2,19 +2,7 @@ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useSta
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
||||||
import {
|
import { Box, Button, Header, Icon, IconButton, Icons, Scroll, Spinner, Text, config } from 'folds';
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
Header,
|
|
||||||
Icon,
|
|
||||||
IconButton,
|
|
||||||
Icons,
|
|
||||||
Scroll,
|
|
||||||
Spinner,
|
|
||||||
Text,
|
|
||||||
config,
|
|
||||||
toRem,
|
|
||||||
} from 'folds';
|
|
||||||
import {
|
import {
|
||||||
Direction,
|
Direction,
|
||||||
EventTimeline,
|
EventTimeline,
|
||||||
|
|
@ -59,12 +47,7 @@ import { ImageViewer } from '../../components/image-viewer';
|
||||||
import { RenderMessageContent } from '../../components/RenderMessageContent';
|
import { RenderMessageContent } from '../../components/RenderMessageContent';
|
||||||
import { RoomInput } from './RoomInput';
|
import { RoomInput } from './RoomInput';
|
||||||
import { RoomInputPlaceholder } from './RoomInputPlaceholder';
|
import { RoomInputPlaceholder } from './RoomInputPlaceholder';
|
||||||
import {
|
import { EncryptedContent, Message, Reactions, useMessageInteractionHandlers } from './message';
|
||||||
EncryptedContent,
|
|
||||||
Message,
|
|
||||||
Reactions,
|
|
||||||
useMessageInteractionHandlers,
|
|
||||||
} from './message';
|
|
||||||
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
|
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
|
||||||
import { useRoomCreators } from '../../hooks/useRoomCreators';
|
import { useRoomCreators } from '../../hooks/useRoomCreators';
|
||||||
import { usePowerLevelsContext } from '../../hooks/usePowerLevels';
|
import { usePowerLevelsContext } from '../../hooks/usePowerLevels';
|
||||||
|
|
@ -76,10 +59,7 @@ import { useTheme } from '../../hooks/useTheme';
|
||||||
import { useImagePackRooms } from '../../hooks/useImagePackRooms';
|
import { useImagePackRooms } from '../../hooks/useImagePackRooms';
|
||||||
import { useRoomCreatorsTag } from '../../hooks/useRoomCreatorsTag';
|
import { useRoomCreatorsTag } from '../../hooks/useRoomCreatorsTag';
|
||||||
import { usePowerLevelTags } from '../../hooks/usePowerLevelTags';
|
import { usePowerLevelTags } from '../../hooks/usePowerLevelTags';
|
||||||
import {
|
import { useAccessiblePowerTagColors, useGetMemberPowerTag } from '../../hooks/useMemberPowerTag';
|
||||||
useAccessiblePowerTagColors,
|
|
||||||
useGetMemberPowerTag,
|
|
||||||
} from '../../hooks/useMemberPowerTag';
|
|
||||||
import { roomToParentsAtom } from '../../state/room/roomToParents';
|
import { roomToParentsAtom } from '../../state/room/roomToParents';
|
||||||
import { draftKey, roomIdToReplyDraftAtomFamily } from '../../state/room/roomInputDrafts';
|
import { draftKey, roomIdToReplyDraftAtomFamily } from '../../state/room/roomInputDrafts';
|
||||||
import { useSetting } from '../../state/hooks/settings';
|
import { useSetting } from '../../state/hooks/settings';
|
||||||
|
|
@ -149,12 +129,7 @@ type ThreadDrawerProps = {
|
||||||
// empty). The component is intentionally light on chrome compared to
|
// empty). The component is intentionally light on chrome compared to
|
||||||
// the full Message renderer in the timeline: M2 is MVP and rich
|
// the full Message renderer in the timeline: M2 is MVP and rich
|
||||||
// reactions / hover-menus / rail-dot inside the drawer is M9 territory.
|
// reactions / hover-menus / rail-dot inside the drawer is M9 territory.
|
||||||
export function ThreadDrawer({
|
export function ThreadDrawer({ room, rootId, parentRoomPath, variant }: ThreadDrawerProps) {
|
||||||
room,
|
|
||||||
rootId,
|
|
||||||
parentRoomPath,
|
|
||||||
variant,
|
|
||||||
}: ThreadDrawerProps) {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
@ -172,9 +147,7 @@ export function ThreadDrawer({
|
||||||
const resizableRef = useRef<HTMLDivElement>(null);
|
const resizableRef = useRef<HTMLDivElement>(null);
|
||||||
const resizeHandleRef = useRef<HTMLDivElement>(null);
|
const resizeHandleRef = useRef<HTMLDivElement>(null);
|
||||||
const [savedWidth, setSavedWidth] = useAtom(threadDrawerWidthAtom);
|
const [savedWidth, setSavedWidth] = useAtom(threadDrawerWidthAtom);
|
||||||
const [vw, setVw] = useState<number>(
|
const [vw, setVw] = useState<number>(typeof window !== 'undefined' ? window.innerWidth : 1280);
|
||||||
typeof window !== 'undefined' ? window.innerWidth : 1280
|
|
||||||
);
|
|
||||||
const [dragging, setDragging] = useState(false);
|
const [dragging, setDragging] = useState(false);
|
||||||
// Live width during a drag — kept in component state so we don't
|
// Live width during a drag — kept in component state so we don't
|
||||||
// flush localStorage on every pointermove (hundreds of sync disk
|
// flush localStorage on every pointermove (hundreds of sync disk
|
||||||
|
|
@ -194,8 +167,7 @@ export function ThreadDrawer({
|
||||||
// Viewports too narrow for any meaningful range (max == min) hide the
|
// Viewports too narrow for any meaningful range (max == min) hide the
|
||||||
// handle entirely — leaving a non-draggable handle reads as broken UI.
|
// handle entirely — leaving a non-draggable handle reads as broken UI.
|
||||||
const canResize = isDesktopVariant && drawerMaxW > THREAD_DRAWER_WIDTH_MIN;
|
const canResize = isDesktopVariant && drawerMaxW > THREAD_DRAWER_WIDTH_MIN;
|
||||||
const atMin =
|
const atMin = dragging && liveWidth !== null && liveWidth <= THREAD_DRAWER_WIDTH_MIN;
|
||||||
dragging && liveWidth !== null && liveWidth <= THREAD_DRAWER_WIDTH_MIN;
|
|
||||||
const atMax = dragging && liveWidth !== null && liveWidth >= drawerMaxW;
|
const atMax = dragging && liveWidth !== null && liveWidth >= drawerMaxW;
|
||||||
|
|
||||||
// Body-style cleanup if the drag terminates without `pointerup`
|
// Body-style cleanup if the drag terminates without `pointerup`
|
||||||
|
|
@ -375,9 +347,7 @@ export function ThreadDrawer({
|
||||||
// The `RoomInput` instance below subscribes to the same atom via
|
// The `RoomInput` instance below subscribes to the same atom via
|
||||||
// `draftKey(roomId, threadId)` (see `roomInputDrafts.ts:34-37`) so
|
// `draftKey(roomId, threadId)` (see `roomInputDrafts.ts:34-37`) so
|
||||||
// the chip surfaces inside the drawer composer.
|
// the chip surfaces inside the drawer composer.
|
||||||
const setReplyDraft = useSetAtom(
|
const setReplyDraft = useSetAtom(roomIdToReplyDraftAtomFamily(draftKey(room.roomId, rootId)));
|
||||||
roomIdToReplyDraftAtomFamily(draftKey(room.roomId, rootId))
|
|
||||||
);
|
|
||||||
|
|
||||||
// Reply-chip click scrolls within the drawer to the matching
|
// Reply-chip click scrolls within the drawer to the matching
|
||||||
// `data-message-id` row. If the target lives in the main timeline
|
// `data-message-id` row. If the target lives in the main timeline
|
||||||
|
|
@ -387,9 +357,7 @@ export function ThreadDrawer({
|
||||||
const handleScrollToDrawerEvent = useCallback((evtId: string) => {
|
const handleScrollToDrawerEvent = useCallback((evtId: string) => {
|
||||||
const host = scrollHostRef.current;
|
const host = scrollHostRef.current;
|
||||||
if (!host) return;
|
if (!host) return;
|
||||||
const target = host.querySelector<HTMLElement>(
|
const target = host.querySelector<HTMLElement>(`[data-message-id="${evtId}"]`);
|
||||||
`[data-message-id="${evtId}"]`
|
|
||||||
);
|
|
||||||
if (target) target.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
if (target) target.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -598,10 +566,7 @@ export function ThreadDrawer({
|
||||||
// resume back-pagination from where this fetch left off.
|
// resume back-pagination from where this fetch left off.
|
||||||
// null means «no more older events» (server reached the
|
// null means «no more older events» (server reached the
|
||||||
// thread start), which correctly disables back-pagination.
|
// thread start), which correctly disables back-pagination.
|
||||||
written.liveTimeline.setPaginationToken(
|
written.liveTimeline.setPaginationToken(result.nextBatch ?? null, Direction.Backward);
|
||||||
result.nextBatch ?? null,
|
|
||||||
Direction.Backward
|
|
||||||
);
|
|
||||||
setThread(written);
|
setThread(written);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -798,9 +763,7 @@ export function ThreadDrawer({
|
||||||
subscribed.push(evt);
|
subscribed.push(evt);
|
||||||
});
|
});
|
||||||
return () => {
|
return () => {
|
||||||
subscribed.forEach((evt) =>
|
subscribed.forEach((evt) => evt.off(MatrixEventEvent.RelationsCreated, handler));
|
||||||
evt.off(MatrixEventEvent.RelationsCreated, handler)
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [rootEvent, repliesIds]);
|
}, [rootEvent, repliesIds]);
|
||||||
|
|
@ -921,8 +884,11 @@ export function ThreadDrawer({
|
||||||
host.scrollTop = host.scrollHeight;
|
host.scrollTop = host.scrollHeight;
|
||||||
isAtBottomRef.current = true;
|
isAtBottomRef.current = true;
|
||||||
tryMarkThreadRead();
|
tryMarkThreadRead();
|
||||||
// Intentional: replies array read inside is stable identity-wise
|
// `replies[last]` is read but the array isn't a dep: the effect is
|
||||||
// because computed inline; lint disable for the false-positive.
|
// keyed on `repliesCount` so it only fires when the array actually
|
||||||
|
// grew, and we always want to inspect the freshest replies snapshot
|
||||||
|
// at that moment. Listing `replies` would just refire on every render
|
||||||
|
// for the same growth event.
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [repliesCount, myUserId, tryMarkThreadRead]);
|
}, [repliesCount, myUserId, tryMarkThreadRead]);
|
||||||
|
|
||||||
|
|
@ -998,12 +964,9 @@ export function ThreadDrawer({
|
||||||
// and `getEditedEvent` return undefined for thread replies even
|
// and `getEditedEvent` return undefined for thread replies even
|
||||||
// when the SDK has fully ingested the relation.
|
// when the SDK has fully ingested the relation.
|
||||||
const eventThread = mEvent.getThread();
|
const eventThread = mEvent.getThread();
|
||||||
const isThreadReply =
|
const isThreadReply = mEvent.threadRootId !== undefined && mEvent.threadRootId !== eventId;
|
||||||
mEvent.threadRootId !== undefined && mEvent.threadRootId !== eventId;
|
|
||||||
const timelineSet =
|
const timelineSet =
|
||||||
isThreadReply && eventThread
|
isThreadReply && eventThread ? eventThread.timelineSet : room.getUnfilteredTimelineSet();
|
||||||
? eventThread.timelineSet
|
|
||||||
: room.getUnfilteredTimelineSet();
|
|
||||||
const reactionRelations = getEventReactions(timelineSet, eventId);
|
const reactionRelations = getEventReactions(timelineSet, eventId);
|
||||||
const reactionList = reactionRelations?.getSortedAnnotationsByKey();
|
const reactionList = reactionRelations?.getSortedAnnotationsByKey();
|
||||||
const hasReactions = reactionList && reactionList.length > 0;
|
const hasReactions = reactionList && reactionList.length > 0;
|
||||||
|
|
@ -1029,8 +992,7 @@ export function ThreadDrawer({
|
||||||
// intentional quotes from auto-fallback chains.
|
// intentional quotes from auto-fallback chains.
|
||||||
const wireRelatesTo = mEvent.getWireContent()['m.relates_to'];
|
const wireRelatesTo = mEvent.getWireContent()['m.relates_to'];
|
||||||
const isFallbackInReplyTo =
|
const isFallbackInReplyTo =
|
||||||
wireRelatesTo?.rel_type === RelationType.Thread &&
|
wireRelatesTo?.rel_type === RelationType.Thread && wireRelatesTo?.is_falling_back !== false;
|
||||||
wireRelatesTo?.is_falling_back !== false;
|
|
||||||
const showReplyChip = !!replyEventId && !isFallbackInReplyTo;
|
const showReplyChip = !!replyEventId && !isFallbackInReplyTo;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -1097,9 +1059,7 @@ export function ThreadDrawer({
|
||||||
{(() => {
|
{(() => {
|
||||||
if (mEvent.isRedacted()) {
|
if (mEvent.isRedacted()) {
|
||||||
return (
|
return (
|
||||||
<RedactedContent
|
<RedactedContent reason={mEvent.getUnsigned().redacted_because?.content?.reason} />
|
||||||
reason={mEvent.getUnsigned().redacted_because?.content?.reason}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (eventType === MessageEvent.Sticker) {
|
if (eventType === MessageEvent.Sticker) {
|
||||||
|
|
@ -1242,21 +1202,24 @@ export function ThreadDrawer({
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{thread && !coldLoadError && !coldLoadFetching && !paginating && !paginateError && replies.length === 0 && (
|
{thread &&
|
||||||
<div className={css.ThreadEmptyState}>
|
!coldLoadError &&
|
||||||
<Text size="T300" priority="400">
|
!coldLoadFetching &&
|
||||||
{t('Room.thread_no_replies')}
|
!paginating &&
|
||||||
</Text>
|
!paginateError &&
|
||||||
</div>
|
replies.length === 0 && (
|
||||||
)}
|
<div className={css.ThreadEmptyState}>
|
||||||
|
<Text size="T300" priority="400">
|
||||||
|
{t('Room.thread_no_replies')}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{/* Top sentinel — IntersectionObserver triggers paginate when
|
{/* Top sentinel — IntersectionObserver triggers paginate when
|
||||||
scrolled into view. Element-web onFillRequest pattern. The
|
scrolled into view. Element-web onFillRequest pattern. The
|
||||||
sentinel only renders while back-pagination is possible:
|
sentinel only renders while back-pagination is possible:
|
||||||
once SDK reports no more events, we drop it so the observer
|
once SDK reports no more events, we drop it so the observer
|
||||||
disconnects on next mount. */}
|
disconnects on next mount. */}
|
||||||
{replies.length > 0 && (
|
{replies.length > 0 && <div ref={setTopSentinel} aria-hidden="true" />}
|
||||||
<div ref={setTopSentinel} aria-hidden="true" />
|
|
||||||
)}
|
|
||||||
{paginating && replies.length > 0 && (
|
{paginating && replies.length > 0 && (
|
||||||
<Box alignItems="Center" justifyContent="Center" style={{ padding: config.space.S200 }}>
|
<Box alignItems="Center" justifyContent="Center" style={{ padding: config.space.S200 }}>
|
||||||
<Spinner size="200" />
|
<Spinner size="200" />
|
||||||
|
|
@ -1336,13 +1299,12 @@ export function ThreadDrawer({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div ref={resizableRef} className={css.ThreadDrawerResizable} style={{ width: drawerWidth }}>
|
||||||
ref={resizableRef}
|
|
||||||
className={css.ThreadDrawerResizable}
|
|
||||||
style={{ width: drawerWidth }}
|
|
||||||
>
|
|
||||||
{asideContent}
|
{asideContent}
|
||||||
{canResize && (
|
{canResize && (
|
||||||
|
// Canonical WAI-ARIA window-splitter pattern (focusable separator
|
||||||
|
// with current/min/max). See Page.tsx for the same justification.
|
||||||
|
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/role-supports-aria-props, jsx-a11y/no-noninteractive-tabindex
|
||||||
<div
|
<div
|
||||||
ref={resizeHandleRef}
|
ref={resizeHandleRef}
|
||||||
role="separator"
|
role="separator"
|
||||||
|
|
@ -1351,6 +1313,7 @@ export function ThreadDrawer({
|
||||||
aria-valuemin={THREAD_DRAWER_WIDTH_MIN}
|
aria-valuemin={THREAD_DRAWER_WIDTH_MIN}
|
||||||
aria-valuemax={drawerMaxW}
|
aria-valuemax={drawerMaxW}
|
||||||
aria-label="Resize thread drawer"
|
aria-label="Resize thread drawer"
|
||||||
|
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className={css.ThreadDrawerResizeHandle}
|
className={css.ThreadDrawerResizeHandle}
|
||||||
data-dragging={dragging || undefined}
|
data-dragging={dragging || undefined}
|
||||||
|
|
|
||||||
|
|
@ -110,8 +110,7 @@ export function CallMessage({
|
||||||
const senderId = aggregate.anchorSenderId;
|
const senderId = aggregate.anchorSenderId;
|
||||||
const isOwnMessage = !!mx.getUserId() && senderId === mx.getUserId();
|
const isOwnMessage = !!mx.getUserId() && senderId === mx.getUserId();
|
||||||
const peerBg = !isOwnMessage;
|
const peerBg = !isOwnMessage;
|
||||||
const senderName =
|
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId) || senderId;
|
||||||
getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId) || senderId;
|
|
||||||
|
|
||||||
const tagColor = memberPowerTag?.color
|
const tagColor = memberPowerTag?.color
|
||||||
? accessibleTagColors?.get(memberPowerTag.color)
|
? accessibleTagColors?.get(memberPowerTag.color)
|
||||||
|
|
@ -148,11 +147,14 @@ export function CallMessage({
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const iconSrc = wasAnswered || ongoing ? Icons.Phone : Icons.PhoneDown;
|
const iconSrc = wasAnswered || ongoing ? Icons.Phone : Icons.PhoneDown;
|
||||||
const iconColor = ongoing
|
let iconColor: string;
|
||||||
? color.Success.Main
|
if (ongoing) {
|
||||||
: wasAnswered
|
iconColor = color.Success.Main;
|
||||||
? color.Surface.OnContainer
|
} else if (wasAnswered) {
|
||||||
: color.Critical.Main;
|
iconColor = color.Surface.OnContainer;
|
||||||
|
} else {
|
||||||
|
iconColor = color.Critical.Main;
|
||||||
|
}
|
||||||
|
|
||||||
const bubbleBody = (
|
const bubbleBody = (
|
||||||
<Box gap="300" alignItems="Center" style={{ minWidth: 0 }}>
|
<Box gap="300" alignItems="Center" style={{ minWidth: 0 }}>
|
||||||
|
|
@ -194,11 +196,7 @@ export function CallMessage({
|
||||||
isOwn={isOwnMessage}
|
isOwn={isOwnMessage}
|
||||||
headerInBubble={channelHeaderInBubble}
|
headerInBubble={channelHeaderInBubble}
|
||||||
avatar={
|
avatar={
|
||||||
<ChannelMessageAvatar
|
<ChannelMessageAvatar room={room} senderId={senderId} senderDisplayName={senderName} />
|
||||||
room={room}
|
|
||||||
senderId={senderId}
|
|
||||||
senderDisplayName={senderName}
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
header={
|
header={
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ import FocusTrap from 'focus-trap-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useHover, useFocusWithin } from 'react-aria';
|
import { useHover, useFocusWithin } from 'react-aria';
|
||||||
import { MatrixEvent, MsgType, Room } from 'matrix-js-sdk';
|
import { MatrixEvent, MsgType, Room } from 'matrix-js-sdk';
|
||||||
|
import type { StateEvents } from 'matrix-js-sdk';
|
||||||
import { Relations } from 'matrix-js-sdk/lib/models/relations';
|
import { Relations } from 'matrix-js-sdk/lib/models/relations';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { RoomPinnedEventsEventContent } from 'matrix-js-sdk/lib/types';
|
import { RoomPinnedEventsEventContent } from 'matrix-js-sdk/lib/types';
|
||||||
|
|
@ -132,7 +133,7 @@ export const MessageAllReactionItem = as<
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Overlay
|
<Overlay
|
||||||
onContextMenu={(evt: any) => {
|
onContextMenu={(evt: React.MouseEvent<HTMLDivElement>) => {
|
||||||
evt.stopPropagation();
|
evt.stopPropagation();
|
||||||
}}
|
}}
|
||||||
open={open}
|
open={open}
|
||||||
|
|
@ -246,7 +247,8 @@ export const MessageSourceCodeItem = as<
|
||||||
: evt.event;
|
: evt.event;
|
||||||
|
|
||||||
const getText = (): string => {
|
const getText = (): string => {
|
||||||
const evtId = mEvent.getId()!;
|
const evtId = mEvent.getId();
|
||||||
|
if (!evtId) return '';
|
||||||
const evtTimeline = room.getTimelineForEvent(evtId);
|
const evtTimeline = room.getTimelineForEvent(evtId);
|
||||||
const edits =
|
const edits =
|
||||||
evtTimeline &&
|
evtTimeline &&
|
||||||
|
|
@ -364,7 +366,7 @@ export const MessagePinItem = as<
|
||||||
if (!isPinned && eventId) {
|
if (!isPinned && eventId) {
|
||||||
pinContent.pinned.push(eventId);
|
pinContent.pinned.push(eventId);
|
||||||
}
|
}
|
||||||
mx.sendStateEvent(room.roomId, StateEvent.RoomPinnedEvents as any, pinContent);
|
mx.sendStateEvent(room.roomId, StateEvent.RoomPinnedEvents as keyof StateEvents, pinContent);
|
||||||
onClose?.();
|
onClose?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -845,7 +847,7 @@ export const Message = as<'div', MessageProps>(
|
||||||
|
|
||||||
const handleContextMenu: MouseEventHandler<HTMLDivElement> = (evt) => {
|
const handleContextMenu: MouseEventHandler<HTMLDivElement> = (evt) => {
|
||||||
if (evt.altKey || !window.getSelection()?.isCollapsed || edit) return;
|
if (evt.altKey || !window.getSelection()?.isCollapsed || edit) return;
|
||||||
const tag = (evt.target as any).tagName;
|
const tag = (evt.target as Element).tagName;
|
||||||
if (typeof tag === 'string' && tag.toLowerCase() === 'a') return;
|
if (typeof tag === 'string' && tag.toLowerCase() === 'a') return;
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
setMenuAnchor({
|
setMenuAnchor({
|
||||||
|
|
@ -918,11 +920,13 @@ export const Message = as<'div', MessageProps>(
|
||||||
returnFocusOnDeactivate={false}
|
returnFocusOnDeactivate={false}
|
||||||
allowTextCustomEmoji
|
allowTextCustomEmoji
|
||||||
onEmojiSelect={(key) => {
|
onEmojiSelect={(key) => {
|
||||||
onReactionToggle(mEvent.getId()!, key);
|
const evtId = mEvent.getId();
|
||||||
|
if (evtId) onReactionToggle(evtId, key);
|
||||||
setEmojiBoardAnchor(undefined);
|
setEmojiBoardAnchor(undefined);
|
||||||
}}
|
}}
|
||||||
onCustomEmojiSelect={(mxc, shortcode) => {
|
onCustomEmojiSelect={(mxc, shortcode) => {
|
||||||
onReactionToggle(mEvent.getId()!, mxc, shortcode);
|
const evtId = mEvent.getId();
|
||||||
|
if (evtId) onReactionToggle(evtId, mxc, shortcode);
|
||||||
setEmojiBoardAnchor(undefined);
|
setEmojiBoardAnchor(undefined);
|
||||||
}}
|
}}
|
||||||
requestClose={() => {
|
requestClose={() => {
|
||||||
|
|
@ -994,7 +998,8 @@ export const Message = as<'div', MessageProps>(
|
||||||
{canSendReaction && (
|
{canSendReaction && (
|
||||||
<MessageQuickReactions
|
<MessageQuickReactions
|
||||||
onReaction={(key, shortcode) => {
|
onReaction={(key, shortcode) => {
|
||||||
onReactionToggle(mEvent.getId()!, key, shortcode);
|
const evtId = mEvent.getId();
|
||||||
|
if (evtId) onReactionToggle(evtId, key, shortcode);
|
||||||
closeMenu();
|
closeMenu();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
@ -1030,7 +1035,7 @@ export const Message = as<'div', MessageProps>(
|
||||||
after={<Icon size="100" src={Icons.ReplyArrow} />}
|
after={<Icon size="100" src={Icons.ReplyArrow} />}
|
||||||
radii="300"
|
radii="300"
|
||||||
data-event-id={mEvent.getId()}
|
data-event-id={mEvent.getId()}
|
||||||
onClick={(evt: any) => {
|
onClick={(evt: Parameters<typeof onReplyClick>[0]) => {
|
||||||
onReplyClick(evt);
|
onReplyClick(evt);
|
||||||
closeMenu();
|
closeMenu();
|
||||||
}}
|
}}
|
||||||
|
|
@ -1051,7 +1056,7 @@ export const Message = as<'div', MessageProps>(
|
||||||
after={<Icon src={Icons.ThreadPlus} size="100" />}
|
after={<Icon src={Icons.ThreadPlus} size="100" />}
|
||||||
radii="300"
|
radii="300"
|
||||||
data-event-id={mEvent.getId()}
|
data-event-id={mEvent.getId()}
|
||||||
onClick={(evt: any) => {
|
onClick={(evt: Parameters<typeof onReplyClick>[0]) => {
|
||||||
onReplyClick(evt, true);
|
onReplyClick(evt, true);
|
||||||
closeMenu();
|
closeMenu();
|
||||||
}}
|
}}
|
||||||
|
|
@ -1173,11 +1178,7 @@ export const Message = as<'div', MessageProps>(
|
||||||
so the bubble header matches the DM-chat rhythm.
|
so the bubble header matches the DM-chat rhythm.
|
||||||
Channels main timeline keeps T400 for the prominent
|
Channels main timeline keeps T400 for the prominent
|
||||||
avatar-and-name row above an unbordered body. */}
|
avatar-and-name row above an unbordered body. */}
|
||||||
<Text
|
<Text as="span" size={channelHeaderInBubble ? 'T200' : 'T400'} truncate>
|
||||||
as="span"
|
|
||||||
size={channelHeaderInBubble ? 'T200' : 'T400'}
|
|
||||||
truncate
|
|
||||||
>
|
|
||||||
<UsernameBold>
|
<UsernameBold>
|
||||||
{isOwnMessage ? t('Direct.message_me_label') : senderDisplayName}
|
{isOwnMessage ? t('Direct.message_me_label') : senderDisplayName}
|
||||||
</UsernameBold>
|
</UsernameBold>
|
||||||
|
|
@ -1269,7 +1270,7 @@ export const Event = as<'div', EventProps>(
|
||||||
|
|
||||||
const handleContextMenu: MouseEventHandler<HTMLDivElement> = (evt) => {
|
const handleContextMenu: MouseEventHandler<HTMLDivElement> = (evt) => {
|
||||||
if (evt.altKey || !window.getSelection()?.isCollapsed) return;
|
if (evt.altKey || !window.getSelection()?.isCollapsed) return;
|
||||||
const tag = (evt.target as any).tagName;
|
const tag = (evt.target as Element).tagName;
|
||||||
if (typeof tag === 'string' && tag.toLowerCase() === 'a') return;
|
if (typeof tag === 'string' && tag.toLowerCase() === 'a') return;
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
setMenuAnchor({
|
setMenuAnchor({
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import {
|
||||||
import { Editor, Transforms } from 'slate';
|
import { Editor, Transforms } from 'slate';
|
||||||
import { ReactEditor } from 'slate-react';
|
import { ReactEditor } from 'slate-react';
|
||||||
import { IContent, IMentions, MatrixEvent, RelationType, Room } from 'matrix-js-sdk';
|
import { IContent, IMentions, MatrixEvent, RelationType, Room } from 'matrix-js-sdk';
|
||||||
|
import type { RoomMessageEventContent } from 'matrix-js-sdk/lib/types';
|
||||||
import { isKeyHotkey } from 'is-hotkey';
|
import { isKeyHotkey } from 'is-hotkey';
|
||||||
import {
|
import {
|
||||||
AUTOCOMPLETE_PREFIXES,
|
AUTOCOMPLETE_PREFIXES,
|
||||||
|
|
@ -80,7 +81,8 @@ export const MessageEditor = as<'div', MessageEditorProps>(
|
||||||
string | undefined,
|
string | undefined,
|
||||||
IMentions | undefined
|
IMentions | undefined
|
||||||
] => {
|
] => {
|
||||||
const evtId = mEvent.getId()!;
|
const evtId = mEvent.getId();
|
||||||
|
if (!evtId) return [undefined, undefined, undefined];
|
||||||
const evtTimeline = room.getTimelineForEvent(evtId);
|
const evtTimeline = room.getTimelineForEvent(evtId);
|
||||||
const editedEvent =
|
const editedEvent =
|
||||||
evtTimeline && getEditedEvent(evtId, mEvent, evtTimeline.getTimelineSet());
|
evtTimeline && getEditedEvent(evtId, mEvent, evtTimeline.getTimelineSet());
|
||||||
|
|
@ -170,7 +172,11 @@ export const MessageEditor = as<'div', MessageEditorProps>(
|
||||||
// /sync round-trip — same lag as M4 receipts. Optimistic
|
// /sync round-trip — same lag as M4 receipts. Optimistic
|
||||||
// local-replace via `mEvent.makeReplaced(localEvent)` is
|
// local-replace via `mEvent.makeReplaced(localEvent)` is
|
||||||
// tracked for follow-up.
|
// tracked for follow-up.
|
||||||
return mx.sendMessage(roomId, mEvent.threadRootId ?? null, content as any);
|
return mx.sendMessage(
|
||||||
|
roomId,
|
||||||
|
mEvent.threadRootId ?? null,
|
||||||
|
content as RoomMessageEventContent
|
||||||
|
);
|
||||||
}, [mx, editor, roomId, mEvent, isMarkdown, getPrevBodyAndFormattedBody])
|
}, [mx, editor, roomId, mEvent, isMarkdown, getPrevBodyAndFormattedBody])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ export const Reactions = as<'div', ReactionsProps>(
|
||||||
})}
|
})}
|
||||||
{reactions.length > 0 && (
|
{reactions.length > 0 && (
|
||||||
<Overlay
|
<Overlay
|
||||||
onContextMenu={(evt: any) => {
|
onContextMenu={(evt: React.MouseEvent<HTMLDivElement>) => {
|
||||||
evt.stopPropagation();
|
evt.stopPropagation();
|
||||||
}}
|
}}
|
||||||
open={!!viewer}
|
open={!!viewer}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@ import { MouseEvent, MouseEventHandler, useCallback, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Editor } from 'slate';
|
import { Editor } from 'slate';
|
||||||
import { ReactEditor } from 'slate-react';
|
import { ReactEditor } from 'slate-react';
|
||||||
import { IContent, Room } from 'matrix-js-sdk';
|
import { EventType, IContent, Room } from 'matrix-js-sdk';
|
||||||
|
import type { ReactionEventContent } from 'matrix-js-sdk/lib/types';
|
||||||
|
|
||||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||||
import { useOpenUserRoomProfile } from '../../../state/hooks/userRoomProfile';
|
import { useOpenUserRoomProfile } from '../../../state/hooks/userRoomProfile';
|
||||||
|
|
@ -14,13 +15,8 @@ import {
|
||||||
getMemberDisplayName,
|
getMemberDisplayName,
|
||||||
getReactionContent,
|
getReactionContent,
|
||||||
} from '../../../utils/room';
|
} from '../../../utils/room';
|
||||||
import {
|
import { eventWithShortcode, factoryEventSentBy, getMxIdLocalPart } from '../../../utils/matrix';
|
||||||
eventWithShortcode,
|
|
||||||
factoryEventSentBy,
|
|
||||||
getMxIdLocalPart,
|
|
||||||
} from '../../../utils/matrix';
|
|
||||||
import { IReplyDraft } from '../../../state/room/roomInputDrafts';
|
import { IReplyDraft } from '../../../state/room/roomInputDrafts';
|
||||||
import { MessageEvent } from '../../../../types/matrix/room';
|
|
||||||
import { getChannelsThreadPath } from '../../../pages/pathUtils';
|
import { getChannelsThreadPath } from '../../../pages/pathUtils';
|
||||||
|
|
||||||
export type UseMessageInteractionHandlersOptions = {
|
export type UseMessageInteractionHandlersOptions = {
|
||||||
|
|
@ -60,15 +56,8 @@ export type MessageInteractionHandlers = {
|
||||||
handleOpenReply: MouseEventHandler;
|
handleOpenReply: MouseEventHandler;
|
||||||
handleUserClick: MouseEventHandler<HTMLButtonElement>;
|
handleUserClick: MouseEventHandler<HTMLButtonElement>;
|
||||||
handleUsernameClick: MouseEventHandler<HTMLButtonElement>;
|
handleUsernameClick: MouseEventHandler<HTMLButtonElement>;
|
||||||
handleReplyClick: (
|
handleReplyClick: (ev: MouseEvent<HTMLButtonElement>, startThread?: boolean) => void;
|
||||||
ev: MouseEvent<HTMLButtonElement>,
|
handleReactionToggle: (targetEventId: string, key: string, shortcode?: string) => void;
|
||||||
startThread?: boolean
|
|
||||||
) => void;
|
|
||||||
handleReactionToggle: (
|
|
||||||
targetEventId: string,
|
|
||||||
key: string,
|
|
||||||
shortcode?: string
|
|
||||||
) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Wiring layer for `<Message>` event-handler props, shared between
|
// Wiring layer for `<Message>` event-handler props, shared between
|
||||||
|
|
@ -166,13 +155,7 @@ export function useMessageInteractionHandlers({
|
||||||
// draft. Bridged rooms (Telegram puppets etc.) have no thread
|
// draft. Bridged rooms (Telegram puppets etc.) have no thread
|
||||||
// semantic on the bridge side — fall back to the legacy
|
// semantic on the bridge side — fall back to the legacy
|
||||||
// m.in_reply_to draft path so messages still go through.
|
// m.in_reply_to draft path so messages still go through.
|
||||||
if (
|
if (startThread && channelsMode && !isBridged && spaceIdOrAlias && roomIdOrAlias) {
|
||||||
startThread &&
|
|
||||||
channelsMode &&
|
|
||||||
!isBridged &&
|
|
||||||
spaceIdOrAlias &&
|
|
||||||
roomIdOrAlias
|
|
||||||
) {
|
|
||||||
// useParams returns the encoded URL value; getChannelsThreadPath
|
// useParams returns the encoded URL value; getChannelsThreadPath
|
||||||
// re-encodes via generatePath so we decode once first to avoid
|
// re-encodes via generatePath so we decode once first to avoid
|
||||||
// double-encoding (matches `routeParent.ts` decode pattern).
|
// double-encoding (matches `routeParent.ts` decode pattern).
|
||||||
|
|
@ -206,16 +189,7 @@ export function useMessageInteractionHandlers({
|
||||||
setTimeout(() => ReactEditor.focus(editor), 100);
|
setTimeout(() => ReactEditor.focus(editor), 100);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[room, setReplyDraft, editor, channelsMode, isBridged, navigate, spaceIdOrAlias, roomIdOrAlias]
|
||||||
room,
|
|
||||||
setReplyDraft,
|
|
||||||
editor,
|
|
||||||
channelsMode,
|
|
||||||
isBridged,
|
|
||||||
navigate,
|
|
||||||
spaceIdOrAlias,
|
|
||||||
roomIdOrAlias,
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleReactionToggle = useCallback(
|
const handleReactionToggle = useCallback(
|
||||||
|
|
@ -224,10 +198,12 @@ export function useMessageInteractionHandlers({
|
||||||
const allReactions = relations?.getSortedAnnotationsByKey() ?? [];
|
const allReactions = relations?.getSortedAnnotationsByKey() ?? [];
|
||||||
const [, reactionsSet] = allReactions.find(([k]) => k === key) ?? [];
|
const [, reactionsSet] = allReactions.find(([k]) => k === key) ?? [];
|
||||||
const reactions = reactionsSet ? Array.from(reactionsSet) : [];
|
const reactions = reactionsSet ? Array.from(reactionsSet) : [];
|
||||||
const myReaction = reactions.find(factoryEventSentBy(mx.getUserId()!));
|
const myUserId = mx.getUserId();
|
||||||
|
const myReaction = myUserId ? reactions.find(factoryEventSentBy(myUserId)) : undefined;
|
||||||
|
|
||||||
if (myReaction && !!myReaction?.isRelation()) {
|
if (myReaction && !!myReaction?.isRelation()) {
|
||||||
mx.redactEvent(room.roomId, myReaction.getId()!);
|
const reactionId = myReaction.getId();
|
||||||
|
if (reactionId) mx.redactEvent(room.roomId, reactionId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const rShortcode =
|
const rShortcode =
|
||||||
|
|
@ -235,8 +211,8 @@ export function useMessageInteractionHandlers({
|
||||||
(reactions.find(eventWithShortcode)?.getContent().shortcode as string | undefined);
|
(reactions.find(eventWithShortcode)?.getContent().shortcode as string | undefined);
|
||||||
mx.sendEvent(
|
mx.sendEvent(
|
||||||
room.roomId,
|
room.roomId,
|
||||||
MessageEvent.Reaction as any,
|
EventType.Reaction,
|
||||||
getReactionContent(targetEventId, key, rShortcode)
|
getReactionContent(targetEventId, key, rShortcode) as ReactionEventContent
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[mx, room]
|
[mx, room]
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,10 @@ export const getImageMsgContent = async (
|
||||||
): Promise<IContent> => {
|
): Promise<IContent> => {
|
||||||
const { file, originalFile, encInfo, metadata } = item;
|
const { file, originalFile, encInfo, metadata } = item;
|
||||||
const [imgError, imgEl] = await to(loadImageElement(getImageFileUrl(originalFile)));
|
const [imgError, imgEl] = await to(loadImageElement(getImageFileUrl(originalFile)));
|
||||||
|
// Non-fatal: upload continues without dimensions/blurhash when the
|
||||||
|
// browser can't decode the image. Surfacing in the dev console helps
|
||||||
|
// diagnose broken codecs without blocking the user's send.
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
if (imgError) console.warn(imgError);
|
if (imgError) console.warn(imgError);
|
||||||
|
|
||||||
const content: IContent = {
|
const content: IContent = {
|
||||||
|
|
@ -85,6 +89,8 @@ export const getVideoMsgContent = async (
|
||||||
const { file, originalFile, encInfo, metadata } = item;
|
const { file, originalFile, encInfo, metadata } = item;
|
||||||
|
|
||||||
const [videoError, videoEl] = await to(loadVideoElement(getVideoFileUrl(originalFile)));
|
const [videoError, videoEl] = await to(loadVideoElement(getVideoFileUrl(originalFile)));
|
||||||
|
// Same justification as image branch above.
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
if (videoError) console.warn(videoError);
|
if (videoError) console.warn(videoError);
|
||||||
|
|
||||||
const content: IContent = {
|
const content: IContent = {
|
||||||
|
|
@ -109,6 +115,7 @@ export const getVideoMsgContent = async (
|
||||||
scaleYDimension(videoEl.videoWidth, 512, videoEl.videoHeight)
|
scaleYDimension(videoEl.videoWidth, 512, videoEl.videoHeight)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
if (thumbError) console.warn(thumbError);
|
if (thumbError) console.warn(thumbError);
|
||||||
content.info = {
|
content.info = {
|
||||||
...getVideoInfo(videoEl, file),
|
...getVideoInfo(videoEl, file),
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,7 @@ export const ReactionViewer = as<'div', ReactionViewerProps>(
|
||||||
key={senderId}
|
key={senderId}
|
||||||
style={{ padding: `0 ${config.space.S200}` }}
|
style={{ padding: `0 ${config.space.S200}` }}
|
||||||
radii="400"
|
radii="400"
|
||||||
onClick={(event) => {
|
onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
openProfile(
|
openProfile(
|
||||||
room.roomId,
|
room.roomId,
|
||||||
space?.roomId,
|
space?.roomId,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
/* eslint-disable react/destructuring-assignment */
|
/* eslint-disable react/destructuring-assignment */
|
||||||
import React, { forwardRef, MouseEventHandler, useCallback, useMemo, useRef } from 'react';
|
import React, { forwardRef, MouseEventHandler, useCallback, useMemo, useRef } from 'react';
|
||||||
import { MatrixEvent, Room } from 'matrix-js-sdk';
|
import { MatrixEvent, Room } from 'matrix-js-sdk';
|
||||||
|
import type { StateEvents } from 'matrix-js-sdk';
|
||||||
import { RoomPinnedEventsEventContent } from 'matrix-js-sdk/lib/types';
|
import { RoomPinnedEventsEventContent } from 'matrix-js-sdk/lib/types';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
|
|
@ -121,7 +122,11 @@ function PinnedMessage({
|
||||||
pinned: content.pinned.filter((id) => id !== eventId),
|
pinned: content.pinned.filter((id) => id !== eventId),
|
||||||
};
|
};
|
||||||
|
|
||||||
return mx.sendStateEvent(room.roomId, StateEvent.RoomPinnedEvents as any, newContent);
|
return mx.sendStateEvent(
|
||||||
|
room.roomId,
|
||||||
|
StateEvent.RoomPinnedEvents as keyof StateEvents,
|
||||||
|
newContent
|
||||||
|
);
|
||||||
}, [room, eventId, mx])
|
}, [room, eventId, mx])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -172,7 +177,7 @@ function PinnedMessage({
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
||||||
const sender = pinnedEvent.getSender()!;
|
const sender = pinnedEvent.getSender() ?? '';
|
||||||
const displayName = getMemberDisplayName(room, sender) ?? getMxIdLocalPart(sender) ?? sender;
|
const displayName = getMemberDisplayName(room, sender) ?? getMxIdLocalPart(sender) ?? sender;
|
||||||
const senderAvatarMxc = getMemberAvatarMxc(room, sender);
|
const senderAvatarMxc = getMemberAvatarMxc(room, sender);
|
||||||
const getContent = (() => pinnedEvent.getContent()) as GetContentCallback;
|
const getContent = (() => pinnedEvent.getContent()) as GetContentCallback;
|
||||||
|
|
@ -245,7 +250,7 @@ export const RoomPinMenu = forwardRef<HTMLDivElement, RoomPinMenuProps>(
|
||||||
({ room, requestClose }, ref) => {
|
({ room, requestClose }, ref) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const userId = mx.getUserId()!;
|
const userId = mx.getSafeUserId();
|
||||||
const powerLevels = usePowerLevelsContext();
|
const powerLevels = usePowerLevelsContext();
|
||||||
const creators = useRoomCreators(room);
|
const creators = useRoomCreators(room);
|
||||||
|
|
||||||
|
|
@ -329,12 +334,12 @@ export const RoomPinMenu = forwardRef<HTMLDivElement, RoomPinMenuProps>(
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[MessageEvent.RoomMessageEncrypted]: (event, displayName) => {
|
[MessageEvent.RoomMessageEncrypted]: (event, displayName) => {
|
||||||
const eventId = event.getId()!;
|
const eventId = event.getId();
|
||||||
const evtTimeline = room.getTimelineForEvent(eventId);
|
const evtTimeline = eventId ? room.getTimelineForEvent(eventId) : undefined;
|
||||||
|
|
||||||
const mEvent = evtTimeline?.getEvents().find((e) => e.getId() === eventId);
|
const mEvent = evtTimeline?.getEvents().find((e) => e.getId() === eventId);
|
||||||
|
|
||||||
if (!mEvent || !evtTimeline) {
|
if (!mEvent || !evtTimeline || !eventId) {
|
||||||
return (
|
return (
|
||||||
<Box grow="Yes" direction="Column">
|
<Box grow="Yes" direction="Column">
|
||||||
<Text size="T400" priority="300">
|
<Text size="T400" priority="300">
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box, Text, Chip } from 'folds';
|
import { Box, Text, Chip } from 'folds';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
import { useAuthedUserId } from '../../../hooks/useAuthedUserId';
|
||||||
import { SequenceCard } from '../../../components/sequence-card';
|
import { SequenceCard } from '../../../components/sequence-card';
|
||||||
import { SequenceCardStyle } from '../styles.css';
|
import { SequenceCardStyle } from '../styles.css';
|
||||||
import { SettingTile } from '../../../components/setting-tile';
|
import { SettingTile } from '../../../components/setting-tile';
|
||||||
|
|
@ -9,18 +9,12 @@ import { copyToClipboard } from '../../../utils/dom';
|
||||||
|
|
||||||
export function MatrixId() {
|
export function MatrixId() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const mx = useMatrixClient();
|
const userId = useAuthedUserId();
|
||||||
const userId = mx.getUserId()!;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box direction="Column" gap="100">
|
<Box direction="Column" gap="100">
|
||||||
<Text size="L400">{t('Settings.matrix_id')}</Text>
|
<Text size="L400">{t('Settings.matrix_id')}</Text>
|
||||||
<SequenceCard
|
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column" gap="400">
|
||||||
className={SequenceCardStyle}
|
|
||||||
variant="Background"
|
|
||||||
direction="Column"
|
|
||||||
gap="400"
|
|
||||||
>
|
|
||||||
<SettingTile
|
<SettingTile
|
||||||
title={userId}
|
title={userId}
|
||||||
after={
|
after={
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import { SequenceCard } from '../../../components/sequence-card';
|
||||||
import { SequenceCardStyle } from '../styles.css';
|
import { SequenceCardStyle } from '../styles.css';
|
||||||
import { SettingTile } from '../../../components/setting-tile';
|
import { SettingTile } from '../../../components/setting-tile';
|
||||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||||
|
import { useAuthedUserId } from '../../../hooks/useAuthedUserId';
|
||||||
import { UserProfile, useUserProfile } from '../../../hooks/useUserProfile';
|
import { UserProfile, useUserProfile } from '../../../hooks/useUserProfile';
|
||||||
import { getMxIdLocalPart, mxcUrlToHttp } from '../../../utils/matrix';
|
import { getMxIdLocalPart, mxcUrlToHttp } from '../../../utils/matrix';
|
||||||
import { UserAvatar } from '../../../components/user-avatar';
|
import { UserAvatar } from '../../../components/user-avatar';
|
||||||
|
|
@ -308,19 +309,13 @@ function ProfileDisplayName({ profile, userId }: ProfileProps) {
|
||||||
|
|
||||||
export function Profile() {
|
export function Profile() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const mx = useMatrixClient();
|
const userId = useAuthedUserId();
|
||||||
const userId = mx.getUserId()!;
|
|
||||||
const profile = useUserProfile(userId);
|
const profile = useUserProfile(userId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box direction="Column" gap="100">
|
<Box direction="Column" gap="100">
|
||||||
<Text size="L400">{t('Settings.profile')}</Text>
|
<Text size="L400">{t('Settings.profile')}</Text>
|
||||||
<SequenceCard
|
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column" gap="400">
|
||||||
className={SequenceCardStyle}
|
|
||||||
variant="Background"
|
|
||||||
direction="Column"
|
|
||||||
gap="400"
|
|
||||||
>
|
|
||||||
<ProfileAvatar userId={userId} profile={profile} />
|
<ProfileAvatar userId={userId} profile={profile} />
|
||||||
<ProfileDisplayName userId={userId} profile={profile} />
|
<ProfileDisplayName userId={userId} profile={profile} />
|
||||||
</SequenceCard>
|
</SequenceCard>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import type { AccountDataEvents } from 'matrix-js-sdk';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Box, Text, IconButton, Icon, Icons, Scroll, Switch, Button } from 'folds';
|
import { Box, Text, IconButton, Icon, Icons, Scroll, Switch, Button } from 'folds';
|
||||||
import { Page, PageContent, PageHeader } from '../../../components/page';
|
import { Page, PageContent, PageHeader } from '../../../components/page';
|
||||||
|
|
@ -27,7 +28,8 @@ export function DeveloperTools({ requestClose }: DeveloperToolsProps) {
|
||||||
|
|
||||||
const submitAccountData: AccountDataSubmitCallback = useCallback(
|
const submitAccountData: AccountDataSubmitCallback = useCallback(
|
||||||
async (type, content) => {
|
async (type, content) => {
|
||||||
await mx.setAccountData(type, content);
|
// Dev tool accepts arbitrary keys; SDK signature wants `keyof AccountDataEvents`.
|
||||||
|
await mx.setAccountData(type as keyof AccountDataEvents, content);
|
||||||
},
|
},
|
||||||
[mx]
|
[mx]
|
||||||
);
|
);
|
||||||
|
|
@ -36,7 +38,11 @@ export function DeveloperTools({ requestClose }: DeveloperToolsProps) {
|
||||||
return (
|
return (
|
||||||
<AccountDataEditor
|
<AccountDataEditor
|
||||||
type={accountDataType ?? undefined}
|
type={accountDataType ?? undefined}
|
||||||
content={accountDataType ? mx.getAccountData(accountDataType)?.getContent() : undefined}
|
content={
|
||||||
|
accountDataType
|
||||||
|
? mx.getAccountData(accountDataType as keyof AccountDataEvents)?.getContent()
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
submitChange={submitAccountData}
|
submitChange={submitAccountData}
|
||||||
requestClose={() => setAccountDataType(undefined)}
|
requestClose={() => setAccountDataType(undefined)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -53,7 +59,11 @@ export function DeveloperTools({ requestClose }: DeveloperToolsProps) {
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box shrink="No">
|
<Box shrink="No">
|
||||||
<IconButton onClick={requestClose} variant="SurfaceVariant" aria-label={t('Settings.close')}>
|
<IconButton
|
||||||
|
onClick={requestClose}
|
||||||
|
variant="SurfaceVariant"
|
||||||
|
aria-label={t('Settings.close')}
|
||||||
|
>
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -141,7 +141,7 @@ function GlobalPackSelector({
|
||||||
if (!room) return null;
|
if (!room) return null;
|
||||||
const roomPackAddresses = roomPacks
|
const roomPackAddresses = roomPacks
|
||||||
.map((pack) => pack.address)
|
.map((pack) => pack.address)
|
||||||
.filter((addr) => addr !== undefined);
|
.filter((addr): addr is PackAddress => addr !== undefined);
|
||||||
const allSelected = roomPackAddresses.every((addr) =>
|
const allSelected = roomPackAddresses.every((addr) =>
|
||||||
selected.find((address) => packAddressEqual(addr, address))
|
selected.find((address) => packAddressEqual(addr, address))
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ export function UserPack({ onViewPack }: UserPackProps) {
|
||||||
if (userPack) {
|
if (userPack) {
|
||||||
onViewPack(userPack);
|
onViewPack(userPack);
|
||||||
} else {
|
} else {
|
||||||
const defaultPack = new ImagePack(mx.getUserId() ?? '', {}, undefined);
|
const defaultPack = new ImagePack(mx.getSafeUserId(), {}, undefined);
|
||||||
onViewPack(defaultPack);
|
onViewPack(defaultPack);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -34,12 +34,7 @@ export function UserPack({ onViewPack }: UserPackProps) {
|
||||||
return (
|
return (
|
||||||
<Box direction="Column" gap="100">
|
<Box direction="Column" gap="100">
|
||||||
<Text size="L400">{t('Settings.default_pack')}</Text>
|
<Text size="L400">{t('Settings.default_pack')}</Text>
|
||||||
<SequenceCard
|
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column" gap="400">
|
||||||
className={SequenceCardStyle}
|
|
||||||
variant="Background"
|
|
||||||
direction="Column"
|
|
||||||
gap="400"
|
|
||||||
>
|
|
||||||
<SettingTile
|
<SettingTile
|
||||||
title={userPack?.meta.name ?? t('Settings.unknown')}
|
title={userPack?.meta.name ?? t('Settings.unknown')}
|
||||||
description={userPack?.meta.attribution}
|
description={userPack?.meta.attribution}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { SequenceCard } from '../../../components/sequence-card';
|
||||||
import { SequenceCardStyle } from '../styles.css';
|
import { SequenceCardStyle } from '../styles.css';
|
||||||
import { SettingTile } from '../../../components/setting-tile';
|
import { SettingTile } from '../../../components/setting-tile';
|
||||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||||
|
import { useAuthedUserId } from '../../../hooks/useAuthedUserId';
|
||||||
import { useUserProfile } from '../../../hooks/useUserProfile';
|
import { useUserProfile } from '../../../hooks/useUserProfile';
|
||||||
import { getMxIdLocalPart } from '../../../utils/matrix';
|
import { getMxIdLocalPart } from '../../../utils/matrix';
|
||||||
import { makePushRuleData, PushRuleData, usePushRule } from '../../../hooks/usePushRule';
|
import { makePushRuleData, PushRuleData, usePushRule } from '../../../hooks/usePushRule';
|
||||||
|
|
@ -114,8 +115,7 @@ function MentionModeSwitcher({ ruleId, pushRules, defaultPushRuleData }: PushRul
|
||||||
|
|
||||||
export function SpecialMessagesNotifications() {
|
export function SpecialMessagesNotifications() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const mx = useMatrixClient();
|
const userId = useAuthedUserId();
|
||||||
const userId = mx.getUserId()!;
|
|
||||||
const { displayName } = useUserProfile(userId);
|
const { displayName } = useUserProfile(userId);
|
||||||
const pushRulesEvt = useAccountData(AccountDataEvent.PushRules);
|
const pushRulesEvt = useAccountData(AccountDataEvent.PushRules);
|
||||||
const pushRules = useMemo(
|
const pushRules = useMemo(
|
||||||
|
|
@ -134,12 +134,7 @@ export function SpecialMessagesNotifications() {
|
||||||
</Badge>
|
</Badge>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<SequenceCard
|
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column" gap="400">
|
||||||
className={SequenceCardStyle}
|
|
||||||
variant="Background"
|
|
||||||
direction="Column"
|
|
||||||
gap="400"
|
|
||||||
>
|
|
||||||
<SettingTile
|
<SettingTile
|
||||||
title={t('Settings.mention_user_id', { userId })}
|
title={t('Settings.mention_user_id', { userId })}
|
||||||
after={
|
after={
|
||||||
|
|
@ -151,12 +146,7 @@ export function SpecialMessagesNotifications() {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</SequenceCard>
|
</SequenceCard>
|
||||||
<SequenceCard
|
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column" gap="400">
|
||||||
className={SequenceCardStyle}
|
|
||||||
variant="Background"
|
|
||||||
direction="Column"
|
|
||||||
gap="400"
|
|
||||||
>
|
|
||||||
<SettingTile
|
<SettingTile
|
||||||
title={
|
title={
|
||||||
displayName
|
displayName
|
||||||
|
|
@ -172,12 +162,7 @@ export function SpecialMessagesNotifications() {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</SequenceCard>
|
</SequenceCard>
|
||||||
<SequenceCard
|
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column" gap="400">
|
||||||
className={SequenceCardStyle}
|
|
||||||
variant="Background"
|
|
||||||
direction="Column"
|
|
||||||
gap="400"
|
|
||||||
>
|
|
||||||
<SettingTile
|
<SettingTile
|
||||||
title={t('Settings.contains_username', { username: getMxIdLocalPart(userId) })}
|
title={t('Settings.contains_username', { username: getMxIdLocalPart(userId) })}
|
||||||
after={
|
after={
|
||||||
|
|
@ -189,12 +174,7 @@ export function SpecialMessagesNotifications() {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</SequenceCard>
|
</SequenceCard>
|
||||||
<SequenceCard
|
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column" gap="400">
|
||||||
className={SequenceCardStyle}
|
|
||||||
variant="Background"
|
|
||||||
direction="Column"
|
|
||||||
gap="400"
|
|
||||||
>
|
|
||||||
<SettingTile
|
<SettingTile
|
||||||
title={t('Settings.mention_room')}
|
title={t('Settings.mention_room')}
|
||||||
after={
|
after={
|
||||||
|
|
@ -206,12 +186,7 @@ export function SpecialMessagesNotifications() {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</SequenceCard>
|
</SequenceCard>
|
||||||
<SequenceCard
|
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column" gap="400">
|
||||||
className={SequenceCardStyle}
|
|
||||||
variant="Background"
|
|
||||||
direction="Column"
|
|
||||||
gap="400"
|
|
||||||
>
|
|
||||||
<SettingTile
|
<SettingTile
|
||||||
title={t('Settings.contains_room')}
|
title={t('Settings.contains_room')}
|
||||||
after={
|
after={
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
|
import type { AccountDataEvents } from 'matrix-js-sdk';
|
||||||
import { useMatrixClient } from './useMatrixClient';
|
import { useMatrixClient } from './useMatrixClient';
|
||||||
import { useAccountDataCallback } from './useAccountDataCallback';
|
import { useAccountDataCallback } from './useAccountDataCallback';
|
||||||
|
|
||||||
export function useAccountData(eventType: string) {
|
export function useAccountData(eventType: string) {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const [event, setEvent] = useState(() => mx.getAccountData(eventType));
|
// Generic hook accepts arbitrary string keys (incl. custom Vojo / Ponies
|
||||||
|
// event-types augmented in sdkAugmentation.d.ts and one-off dev-tool keys).
|
||||||
|
// Cast through `keyof AccountDataEvents` to satisfy the SDK literal-union.
|
||||||
|
const [event, setEvent] = useState(() => mx.getAccountData(eventType as keyof AccountDataEvents));
|
||||||
|
|
||||||
useAccountDataCallback(
|
useAccountDataCallback(
|
||||||
mx,
|
mx,
|
||||||
|
|
|
||||||
16
src/app/hooks/useAuthedUserId.ts
Normal file
16
src/app/hooks/useAuthedUserId.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { useMatrixClient } from './useMatrixClient';
|
||||||
|
|
||||||
|
// Returns the current user's MXID in any subtree that mounts only after
|
||||||
|
// authentication has resolved (`<ClientLayout>` and everything inside it).
|
||||||
|
// Throws if the SDK has no user ID — that's an invariant violation, not a
|
||||||
|
// soft-failure path: by the time post-login components render, the SDK
|
||||||
|
// already has a session. Prefer this over `mx.getUserId() ?? ''` so a
|
||||||
|
// broken invariant surfaces loudly instead of silently defaulting to '' and
|
||||||
|
// writing empty-string user IDs into account-data / storage keys.
|
||||||
|
//
|
||||||
|
// For utility functions that aren't React hooks, call `mx.getSafeUserId()`
|
||||||
|
// directly — it's the same contract.
|
||||||
|
export function useAuthedUserId(): string {
|
||||||
|
const mx = useMatrixClient();
|
||||||
|
return mx.getSafeUserId();
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,7 @@ import {
|
||||||
RoomMember,
|
RoomMember,
|
||||||
Visibility,
|
Visibility,
|
||||||
} from 'matrix-js-sdk';
|
} from 'matrix-js-sdk';
|
||||||
|
import type { StateEvents } from 'matrix-js-sdk';
|
||||||
import { RoomServerAclEventContent } from 'matrix-js-sdk/lib/types';
|
import { RoomServerAclEventContent } from 'matrix-js-sdk/lib/types';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
|
|
@ -366,7 +367,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
|
||||||
if (!content) return;
|
if (!content) return;
|
||||||
await mx.sendStateEvent(
|
await mx.sendStateEvent(
|
||||||
room.roomId,
|
room.roomId,
|
||||||
StateEvent.RoomMember as any,
|
StateEvent.RoomMember as keyof StateEvents,
|
||||||
{
|
{
|
||||||
...content,
|
...content,
|
||||||
displayname: nick,
|
displayname: nick,
|
||||||
|
|
@ -388,7 +389,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
|
||||||
if (!content) return;
|
if (!content) return;
|
||||||
await mx.sendStateEvent(
|
await mx.sendStateEvent(
|
||||||
room.roomId,
|
room.roomId,
|
||||||
StateEvent.RoomMember as any,
|
StateEvent.RoomMember as keyof StateEvents,
|
||||||
{
|
{
|
||||||
...content,
|
...content,
|
||||||
avatar_url: payload,
|
avatar_url: payload,
|
||||||
|
|
@ -528,7 +529,11 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
|
||||||
aclContent.allow?.sort();
|
aclContent.allow?.sort();
|
||||||
aclContent.deny?.sort();
|
aclContent.deny?.sort();
|
||||||
|
|
||||||
await mx.sendStateEvent(room.roomId, StateEvent.RoomServerAcl as any, aclContent);
|
await mx.sendStateEvent(
|
||||||
|
room.roomId,
|
||||||
|
StateEvent.RoomServerAcl as keyof StateEvents,
|
||||||
|
aclContent
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -37,20 +37,20 @@ export function useDotColor(
|
||||||
hideReadReceipts = false
|
hideReadReceipts = false
|
||||||
): DotColor {
|
): DotColor {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const myUserId = mx.getUserId() ?? '';
|
const myUserId = mx.getSafeUserId();
|
||||||
const senderId = mEvent.getSender() ?? '';
|
const senderId = mEvent.getSender() ?? '';
|
||||||
const isOwn = !!myUserId && senderId === myUserId;
|
const isOwn = senderId === myUserId;
|
||||||
const eventId = mEvent.getId();
|
const eventId = mEvent.getId();
|
||||||
|
|
||||||
const status = useMessageStatus(room, mEvent);
|
const status = useMessageStatus(room, mEvent);
|
||||||
|
|
||||||
const [haveSeen, setHaveSeen] = useState<boolean>(() =>
|
const [haveSeen, setHaveSeen] = useState<boolean>(() =>
|
||||||
enabled && !isOwn && !!eventId && !!myUserId ? room.hasUserReadEvent(myUserId, eventId) : false
|
enabled && !isOwn && !!eventId ? room.hasUserReadEvent(myUserId, eventId) : false
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!enabled) return undefined;
|
if (!enabled) return undefined;
|
||||||
if (isOwn || !eventId || !myUserId) return undefined;
|
if (isOwn || !eventId) return undefined;
|
||||||
setHaveSeen(room.hasUserReadEvent(myUserId, eventId));
|
setHaveSeen(room.hasUserReadEvent(myUserId, eventId));
|
||||||
const handler: RoomEventHandlerMap[RoomEvent.Receipt] = (_event, r) => {
|
const handler: RoomEventHandlerMap[RoomEvent.Receipt] = (_event, r) => {
|
||||||
if (r.roomId !== room.roomId) return;
|
if (r.roomId !== room.roomId) return;
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ function getDeliveryStatus(
|
||||||
|
|
||||||
export function useMessageStatus(room: Room, mEvent: MatrixEvent): MessageDeliveryStatus | null {
|
export function useMessageStatus(room: Room, mEvent: MatrixEvent): MessageDeliveryStatus | null {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const myUserId = mx.getUserId() ?? '';
|
const myUserId = mx.getSafeUserId();
|
||||||
const isOwnMessage = mEvent.getSender() === myUserId;
|
const isOwnMessage = mEvent.getSender() === myUserId;
|
||||||
|
|
||||||
const [status, setStatus] = useState<MessageDeliveryStatus | null>(() =>
|
const [status, setStatus] = useState<MessageDeliveryStatus | null>(() =>
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,8 @@ const sortPowers = (powers: number[]): number[] => powers.sort(powerSortFn);
|
||||||
|
|
||||||
export const getPowers = (tags: PowerLevelTags): number[] => {
|
export const getPowers = (tags: PowerLevelTags): number[] => {
|
||||||
const powers: number[] = Object.keys(tags)
|
const powers: number[] = Object.keys(tags)
|
||||||
.map((p) => {
|
.map((p) => parseInt(p, 10))
|
||||||
const power = parseInt(p, 10);
|
.filter((power): power is number => !Number.isNaN(power));
|
||||||
if (Number.isNaN(power)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return power;
|
|
||||||
})
|
|
||||||
.filter((power) => typeof power === 'number');
|
|
||||||
|
|
||||||
return sortPowers(powers);
|
return sortPowers(powers);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,8 @@ const fillMissingPowers = (powerLevels: IPowerLevels): IPowerLevels =>
|
||||||
const keys = Object.keys(DEFAULT_POWER_LEVELS) as unknown as (keyof IPowerLevels)[];
|
const keys = Object.keys(DEFAULT_POWER_LEVELS) as unknown as (keyof IPowerLevels)[];
|
||||||
keys.forEach((key) => {
|
keys.forEach((key) => {
|
||||||
if (draftPl[key] === undefined) {
|
if (draftPl[key] === undefined) {
|
||||||
// eslint-disable-next-line no-param-reassign
|
// Distributive union over IPowerLevels keys breaks TS narrowing across the indexed write.
|
||||||
|
// eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any
|
||||||
draftPl[key] = DEFAULT_POWER_LEVELS[key] as any;
|
draftPl[key] = DEFAULT_POWER_LEVELS[key] as any;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useCallback, useEffect, useMemo } from 'react';
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
import { MatrixError, Room } from 'matrix-js-sdk';
|
import { MatrixError, Room } from 'matrix-js-sdk';
|
||||||
|
import type { StateEvents } from 'matrix-js-sdk';
|
||||||
import { RoomCanonicalAliasEventContent } from 'matrix-js-sdk/lib/types';
|
import { RoomCanonicalAliasEventContent } from 'matrix-js-sdk/lib/types';
|
||||||
import { AsyncState, useAsyncCallback } from './useAsyncCallback';
|
import { AsyncState, useAsyncCallback } from './useAsyncCallback';
|
||||||
import { useMatrixClient } from './useMatrixClient';
|
import { useMatrixClient } from './useMatrixClient';
|
||||||
|
|
@ -56,7 +57,11 @@ export const useSetMainAlias = (room: Room): ((alias: string | undefined) => Pro
|
||||||
alt_aliases: altAliases,
|
alt_aliases: altAliases,
|
||||||
};
|
};
|
||||||
|
|
||||||
await mx.sendStateEvent(room.roomId, StateEvent.RoomCanonicalAlias as any, newContent);
|
await mx.sendStateEvent(
|
||||||
|
room.roomId,
|
||||||
|
StateEvent.RoomCanonicalAlias as keyof StateEvents,
|
||||||
|
newContent
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[mx, room]
|
[mx, room]
|
||||||
);
|
);
|
||||||
|
|
@ -90,7 +95,11 @@ export const usePublishUnpublishAliases = (
|
||||||
alt_aliases: altAliases,
|
alt_aliases: altAliases,
|
||||||
};
|
};
|
||||||
|
|
||||||
await mx.sendStateEvent(room.roomId, StateEvent.RoomCanonicalAlias as any, newContent);
|
await mx.sendStateEvent(
|
||||||
|
room.roomId,
|
||||||
|
StateEvent.RoomCanonicalAlias as keyof StateEvents,
|
||||||
|
newContent
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[mx, room]
|
[mx, room]
|
||||||
);
|
);
|
||||||
|
|
@ -114,7 +123,11 @@ export const usePublishUnpublishAliases = (
|
||||||
alt_aliases: altAliases,
|
alt_aliases: altAliases,
|
||||||
};
|
};
|
||||||
|
|
||||||
await mx.sendStateEvent(room.roomId, StateEvent.RoomCanonicalAlias as any, newContent);
|
await mx.sendStateEvent(
|
||||||
|
room.roomId,
|
||||||
|
StateEvent.RoomCanonicalAlias as keyof StateEvents,
|
||||||
|
newContent
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[mx, room]
|
[mx, room]
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,6 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types';
|
import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types';
|
||||||
import {
|
import { Room, RoomEvent, RoomStateEvent } from 'matrix-js-sdk';
|
||||||
Room,
|
|
||||||
RoomEvent,
|
|
||||||
RoomEventHandlerMap,
|
|
||||||
RoomStateEvent,
|
|
||||||
} from 'matrix-js-sdk';
|
|
||||||
import { StateEvent } from '../../types/matrix/room';
|
import { StateEvent } from '../../types/matrix/room';
|
||||||
import { useStateEvent } from './useStateEvent';
|
import { useStateEvent } from './useStateEvent';
|
||||||
|
|
||||||
|
|
@ -39,9 +34,7 @@ const resolveRoomName = (room: Room): string => {
|
||||||
if (peer.rawDisplayName && peer.rawDisplayName.trim()) {
|
if (peer.rawDisplayName && peer.rawDisplayName.trim()) {
|
||||||
return peer.rawDisplayName.trim();
|
return peer.rawDisplayName.trim();
|
||||||
}
|
}
|
||||||
const local = peer.userId.startsWith('@')
|
const local = peer.userId.startsWith('@') ? peer.userId.slice(1).split(':')[0] : peer.userId;
|
||||||
? peer.userId.slice(1).split(':')[0]
|
|
||||||
: peer.userId;
|
|
||||||
return local || peer.userId;
|
return local || peer.userId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -67,7 +60,10 @@ export const useRoomName = (room: Room): string => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setName(resolveRoomName(room));
|
setName(resolveRoomName(room));
|
||||||
|
|
||||||
const recompute: RoomEventHandlerMap[RoomEvent.Name] = () => {
|
// No-arg form keeps the same callback assignable to both RoomEvent.Name
|
||||||
|
// (event handler is `(room: Room) => void`) and RoomStateEvent.Members
|
||||||
|
// (`(event, state, member) => void`); we ignore the args either way.
|
||||||
|
const recompute = () => {
|
||||||
setName(resolveRoomName(room));
|
setName(resolveRoomName(room));
|
||||||
};
|
};
|
||||||
// RoomEvent.Name fires when m.room.name changes;
|
// RoomEvent.Name fires when m.room.name changes;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,7 @@
|
||||||
import React, { Ref, useCallback, useEffect, useLayoutEffect, useRef } from 'react';
|
import React, { Ref, useCallback, useEffect, useLayoutEffect, useRef } from 'react';
|
||||||
|
|
||||||
import { launchSplash } from '../../plugins/launchSplash';
|
import { launchSplash } from '../../plugins/launchSplash';
|
||||||
import {
|
import { attachMascotVideo, detachMascotVideo, mascotVideoReady } from './mascotSingleton';
|
||||||
attachMascotVideo,
|
|
||||||
detachMascotVideo,
|
|
||||||
mascotVideoReady,
|
|
||||||
} from './mascotSingleton';
|
|
||||||
import * as css from './styles.css';
|
import * as css from './styles.css';
|
||||||
|
|
||||||
type AuthMascotProps = {
|
type AuthMascotProps = {
|
||||||
|
|
@ -64,8 +60,13 @@ export function AuthMascot({ mascotRef, centered }: AuthMascotProps) {
|
||||||
const setRefs = useCallback(
|
const setRefs = useCallback(
|
||||||
(node: HTMLDivElement | null) => {
|
(node: HTMLDivElement | null) => {
|
||||||
containerRef.current = node;
|
containerRef.current = node;
|
||||||
if (typeof mascotRef === 'function') mascotRef(node);
|
if (typeof mascotRef === 'function') {
|
||||||
else if (mascotRef && typeof mascotRef === 'object') {
|
mascotRef(node);
|
||||||
|
} else if (mascotRef && typeof mascotRef === 'object') {
|
||||||
|
// Forward-ref mutation is the canonical React pattern for combining
|
||||||
|
// refs; the eslint rule fires on any param mutation, but a MutableRefObject
|
||||||
|
// is specifically designed for this.
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
(mascotRef as React.MutableRefObject<HTMLDivElement | null>).current = node;
|
(mascotRef as React.MutableRefObject<HTMLDivElement | null>).current = node;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { ReactNode, useMemo } from 'react';
|
import React, { ReactNode, useMemo } from 'react';
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
import { useAuthedUserId } from '../../hooks/useAuthedUserId';
|
||||||
import { makeClosedNavCategoriesAtom } from '../../state/closedNavCategories';
|
import { makeClosedNavCategoriesAtom } from '../../state/closedNavCategories';
|
||||||
import { ClosedNavCategoriesProvider } from '../../state/hooks/closedNavCategories';
|
import { ClosedNavCategoriesProvider } from '../../state/hooks/closedNavCategories';
|
||||||
import { makeClosedLobbyCategoriesAtom } from '../../state/closedLobbyCategories';
|
import { makeClosedLobbyCategoriesAtom } from '../../state/closedLobbyCategories';
|
||||||
|
|
@ -15,8 +15,7 @@ type ClientInitStorageAtomProps = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
};
|
};
|
||||||
export function ClientInitStorageAtom({ children }: ClientInitStorageAtomProps) {
|
export function ClientInitStorageAtom({ children }: ClientInitStorageAtomProps) {
|
||||||
const mx = useMatrixClient();
|
const userId = useAuthedUserId();
|
||||||
const userId = mx.getUserId()!;
|
|
||||||
|
|
||||||
const closedNavCategoriesAtom = useMemo(() => makeClosedNavCategoriesAtom(userId), [userId]);
|
const closedNavCategoriesAtom = useMemo(() => makeClosedNavCategoriesAtom(userId), [userId]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,7 @@ import { Box, Button, config, Dialog, Text } from 'folds';
|
||||||
import { HttpApiEvent, HttpApiEventHandlerMap, MatrixClient, SyncState } from 'matrix-js-sdk';
|
import { HttpApiEvent, HttpApiEventHandlerMap, MatrixClient, SyncState } from 'matrix-js-sdk';
|
||||||
import React, { ReactNode, useCallback, useEffect, useState } from 'react';
|
import React, { ReactNode, useCallback, useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import { clearLocalSessionAndReload, initClient, startClient } from '../../../client/initMatrix';
|
||||||
clearLocalSessionAndReload,
|
|
||||||
initClient,
|
|
||||||
startClient,
|
|
||||||
} from '../../../client/initMatrix';
|
|
||||||
import { ServerConfigsLoader } from '../../components/ServerConfigsLoader';
|
import { ServerConfigsLoader } from '../../components/ServerConfigsLoader';
|
||||||
import { CapabilitiesProvider } from '../../hooks/useCapabilities';
|
import { CapabilitiesProvider } from '../../hooks/useCapabilities';
|
||||||
import { MediaConfigProvider } from '../../hooks/useMediaConfig';
|
import { MediaConfigProvider } from '../../hooks/useMediaConfig';
|
||||||
|
|
@ -138,9 +134,15 @@ export function ClientRoot({ children }: ClientRootProps) {
|
||||||
onRetry = loadMatrix;
|
onRetry = loadMatrix;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When `getFallbackSession()` returns `undefined` (no stored session),
|
||||||
|
// `loadMatrix()` throws and the error branch below renders inside
|
||||||
|
// `AuthSplashScreen`. The empty-string defaults reach AutoDiscovery /
|
||||||
|
// SpecVersions but neither component does anything load-bearing with them
|
||||||
|
// in the error path — AutoDiscovery falls back to an empty `m.homeserver`
|
||||||
|
// record, SpecVersions probes the current origin (harmless).
|
||||||
return (
|
return (
|
||||||
<AutoDiscovery userId={userId!} baseUrl={baseUrl!}>
|
<AutoDiscovery userId={userId ?? ''} baseUrl={baseUrl ?? ''}>
|
||||||
<SpecVersions baseUrl={baseUrl!}>
|
<SpecVersions baseUrl={baseUrl ?? ''}>
|
||||||
{mx && <SyncIndicator mx={mx} />}
|
{mx && <SyncIndicator mx={mx} />}
|
||||||
{(loadState.status === AsyncStatus.Error ||
|
{(loadState.status === AsyncStatus.Error ||
|
||||||
startState.status === AsyncStatus.Error ||
|
startState.status === AsyncStatus.Error ||
|
||||||
|
|
@ -168,9 +170,7 @@ export function ClientRoot({ children }: ClientRootProps) {
|
||||||
{loading &&
|
{loading &&
|
||||||
syncErrored &&
|
syncErrored &&
|
||||||
loadState.status !== AsyncStatus.Error &&
|
loadState.status !== AsyncStatus.Error &&
|
||||||
startState.status !== AsyncStatus.Error && (
|
startState.status !== AsyncStatus.Error && <Text>{t('Boot.sync_failed')}</Text>}
|
||||||
<Text>{t('Boot.sync_failed')}</Text>
|
|
||||||
)}
|
|
||||||
<Button variant="Critical" onClick={onRetry}>
|
<Button variant="Critical" onClick={onRetry}>
|
||||||
<Text as="span" size="B400">
|
<Text as="span" size="B400">
|
||||||
{t('Boot.retry')}
|
{t('Boot.retry')}
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,8 @@ export function DirectRouteRoomProvider({ children }: { children: ReactNode }) {
|
||||||
// After P3c the Direct tab is universal — any joined non-space room renders
|
// After P3c the Direct tab is universal — any joined non-space room renders
|
||||||
// here. Spaces (= future Channels) keep their own /{spaceId}/ route.
|
// here. Spaces (= future Channels) keep their own /{spaceId}/ route.
|
||||||
if (!room || isSpace(room)) {
|
if (!room || isSpace(room)) {
|
||||||
return <JoinBeforeNavigate roomIdOrAlias={roomIdOrAlias!} eventId={eventId} />;
|
if (!roomIdOrAlias) return <Navigate to={getDirectPath()} replace />;
|
||||||
|
return <JoinBeforeNavigate roomIdOrAlias={roomIdOrAlias} eventId={eventId} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ResolvedRoomProvider room={room}>{children}</ResolvedRoomProvider>;
|
return <ResolvedRoomProvider room={room}>{children}</ResolvedRoomProvider>;
|
||||||
|
|
|
||||||
|
|
@ -43,21 +43,15 @@ export function HomeRouteRoomProvider({ children: _children }: { children: React
|
||||||
const alias = getCanonicalAliasOrRoomId(mx, room.roomId);
|
const alias = getCanonicalAliasOrRoomId(mx, room.roomId);
|
||||||
const orphanParents = getOrphanParents(roomToParents, room.roomId);
|
const orphanParents = getOrphanParents(roomToParents, room.roomId);
|
||||||
if (orphanParents.length > 0) {
|
if (orphanParents.length > 0) {
|
||||||
const parentSpace =
|
const parentSpace = guessPerfectParent(mx, room.roomId, orphanParents) ?? orphanParents[0];
|
||||||
guessPerfectParent(mx, room.roomId, orphanParents) ?? orphanParents[0];
|
|
||||||
const pSpaceIdOrAlias = getCanonicalAliasOrRoomId(mx, parentSpace);
|
const pSpaceIdOrAlias = getCanonicalAliasOrRoomId(mx, parentSpace);
|
||||||
return (
|
return <Navigate to={getChannelsRoomPath(pSpaceIdOrAlias, alias, eventId)} replace />;
|
||||||
<Navigate to={getChannelsRoomPath(pSpaceIdOrAlias, alias, eventId)} replace />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return <Navigate to={getDirectRoomPath(alias, eventId)} replace />;
|
return <Navigate to={getDirectRoomPath(alias, eventId)} replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!roomIdOrAlias) return <Navigate to="/direct" replace />;
|
||||||
return (
|
return (
|
||||||
<JoinBeforeNavigate
|
<JoinBeforeNavigate roomIdOrAlias={roomIdOrAlias} eventId={eventId} viaServers={viaServers} />
|
||||||
roomIdOrAlias={roomIdOrAlias!}
|
|
||||||
eventId={eventId}
|
|
||||||
viaServers={viaServers}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { useMatch, useNavigate } from 'react-router-dom';
|
||||||
import { SidebarItem, SidebarItemTooltip, SidebarAvatar } from '../../../components/sidebar';
|
import { SidebarItem, SidebarItemTooltip, SidebarAvatar } from '../../../components/sidebar';
|
||||||
import { UserAvatar } from '../../../components/user-avatar';
|
import { UserAvatar } from '../../../components/user-avatar';
|
||||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||||
|
import { useAuthedUserId } from '../../../hooks/useAuthedUserId';
|
||||||
import { getMxIdLocalPart, mxcUrlToHttp } from '../../../utils/matrix';
|
import { getMxIdLocalPart, mxcUrlToHttp } from '../../../utils/matrix';
|
||||||
import { nameInitials } from '../../../utils/common';
|
import { nameInitials } from '../../../utils/common';
|
||||||
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
|
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
|
||||||
|
|
@ -17,7 +18,7 @@ export function SettingsTab() {
|
||||||
const useAuthentication = useMediaAuthentication();
|
const useAuthentication = useMediaAuthentication();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const settingsMatch = useMatch({ path: SETTINGS_PATH, caseSensitive: true, end: false });
|
const settingsMatch = useMatch({ path: SETTINGS_PATH, caseSensitive: true, end: false });
|
||||||
const userId = mx.getUserId()!;
|
const userId = useAuthedUserId();
|
||||||
const profile = useUserProfile(userId);
|
const profile = useUserProfile(userId);
|
||||||
|
|
||||||
const displayName = profile.displayName ?? getMxIdLocalPart(userId) ?? userId;
|
const displayName = profile.displayName ?? getMxIdLocalPart(userId) ?? userId;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import { Room } from 'matrix-js-sdk';
|
import { Room } from 'matrix-js-sdk';
|
||||||
import { useParams } from 'react-router-dom';
|
import { Navigate, useParams } from 'react-router-dom';
|
||||||
import { useAtom, useAtomValue } from 'jotai';
|
import { useAtom, useAtomValue } from 'jotai';
|
||||||
import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom';
|
import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom';
|
||||||
import { IsOneOnOneProvider, RoomProvider } from '../../../hooks/useRoom';
|
import { IsOneOnOneProvider, RoomProvider } from '../../../hooks/useRoom';
|
||||||
|
|
@ -41,12 +41,9 @@ export function SpaceRouteRoomProvider({ children }: { children: ReactNode }) {
|
||||||
|
|
||||||
if (!room || !allRooms.includes(room.roomId)) {
|
if (!room || !allRooms.includes(room.roomId)) {
|
||||||
// room is not joined
|
// room is not joined
|
||||||
|
if (!roomIdOrAlias) return <Navigate to="/direct" replace />;
|
||||||
return (
|
return (
|
||||||
<JoinBeforeNavigate
|
<JoinBeforeNavigate roomIdOrAlias={roomIdOrAlias} eventId={eventId} viaServers={viaServers} />
|
||||||
roomIdOrAlias={roomIdOrAlias!}
|
|
||||||
eventId={eventId}
|
|
||||||
viaServers={viaServers}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -67,12 +64,9 @@ export function SpaceRouteRoomProvider({ children }: { children: ReactNode }) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!roomIdOrAlias) return <Navigate to="/direct" replace />;
|
||||||
return (
|
return (
|
||||||
<JoinBeforeNavigate
|
<JoinBeforeNavigate roomIdOrAlias={roomIdOrAlias} eventId={eventId} viaServers={viaServers} />
|
||||||
roomIdOrAlias={roomIdOrAlias!}
|
|
||||||
eventId={eventId}
|
|
||||||
viaServers={viaServers}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -343,7 +343,8 @@ export function SpaceTombstone({ roomId, replacementRoomId }: SpaceTombstoneProp
|
||||||
<Text size="T200">This space has been replaced and is no longer active.</Text>
|
<Text size="T200">This space has been replaced and is no longer active.</Text>
|
||||||
{joinState.status === AsyncStatus.Error && (
|
{joinState.status === AsyncStatus.Error && (
|
||||||
<Text className={BreakWord} style={{ color: color.Critical.Main }} size="T200">
|
<Text className={BreakWord} style={{ color: color.Critical.Main }} size="T200">
|
||||||
{(joinState.error as any)?.message ?? 'Failed to join replacement space!'}
|
{(joinState.error as { message?: string } | undefined)?.message ??
|
||||||
|
'Failed to join replacement space!'}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -126,8 +126,15 @@ export class CallEmbed {
|
||||||
const iframe = document.createElement('iframe');
|
const iframe = document.createElement('iframe');
|
||||||
|
|
||||||
iframe.title = 'Call Embed';
|
iframe.title = 'Call Embed';
|
||||||
iframe.sandbox =
|
// `HTMLIFrameElement.sandbox` is typed as a `DOMTokenList` in modern
|
||||||
'allow-forms allow-scripts allow-same-origin allow-popups allow-modals allow-downloads';
|
// lib.dom; string assignment is rejected by the `--strict` compiler even
|
||||||
|
// though browsers still honour it via `[PutForwards=value]`. Use
|
||||||
|
// `setAttribute` for an unambiguous, strict-typed write. Must be set
|
||||||
|
// BEFORE `iframe.src` so the sandbox policy applies at first navigation.
|
||||||
|
iframe.setAttribute(
|
||||||
|
'sandbox',
|
||||||
|
'allow-forms allow-scripts allow-same-origin allow-popups allow-modals allow-downloads'
|
||||||
|
);
|
||||||
iframe.allow = 'microphone; camera; display-capture; autoplay; clipboard-write;';
|
iframe.allow = 'microphone; camera; display-capture; autoplay; clipboard-write;';
|
||||||
iframe.src = url;
|
iframe.src = url;
|
||||||
|
|
||||||
|
|
@ -201,7 +208,7 @@ export class CallEmbed {
|
||||||
return this.listenEvent('preparing', callback);
|
return this.listenEvent('preparing', callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onPreparingError(callback: (error: any) => void) {
|
public onPreparingError(callback: (error: unknown) => void) {
|
||||||
return this.listenEvent('error:preparing', callback);
|
return this.listenEvent('error:preparing', callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -228,7 +235,9 @@ export class CallEmbed {
|
||||||
const events = room.getLiveTimeline()?.getEvents() || [];
|
const events = room.getLiveTimeline()?.getEvents() || [];
|
||||||
const roomEvent = events[events.length - 1];
|
const roomEvent = events[events.length - 1];
|
||||||
if (!roomEvent) return; // force later code to think the room is fresh
|
if (!roomEvent) return; // force later code to think the room is fresh
|
||||||
this.readUpToMap[room.roomId] = roomEvent.getId()!;
|
const evtId = roomEvent.getId();
|
||||||
|
if (!evtId) return;
|
||||||
|
this.readUpToMap[room.roomId] = evtId;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Attach listeners for feeding events - the underlying widget classes handle permissions for us
|
// Attach listeners for feeding events - the underlying widget classes handle permissions for us
|
||||||
|
|
@ -296,6 +305,9 @@ export class CallEmbed {
|
||||||
if (this.call === null) return;
|
if (this.call === null) return;
|
||||||
const raw = ev.getEffectiveEvent();
|
const raw = ev.getEffectiveEvent();
|
||||||
this.call.feedStateUpdate(raw as IRoomEvent).catch((e) => {
|
this.call.feedStateUpdate(raw as IRoomEvent).catch((e) => {
|
||||||
|
// Element Call widget channel error — surface so a stuck widget is
|
||||||
|
// diagnosable without breaking the call lifecycle.
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.error('Error sending state update to widget: ', e);
|
console.error('Error sending state update to widget: ', e);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -329,7 +341,7 @@ export class CallEmbed {
|
||||||
const room = this.mx.getRoom(roomId);
|
const room = this.mx.getRoom(roomId);
|
||||||
if (room === null) return false;
|
if (room === null) return false;
|
||||||
|
|
||||||
const upToEventId = this.readUpToMap[ev.getRoomId()!];
|
const upToEventId = this.readUpToMap[roomId];
|
||||||
if (!upToEventId) {
|
if (!upToEventId) {
|
||||||
// There's no marker yet; start it at this event
|
// There's no marker yet; start it at this event
|
||||||
this.readUpToMap[roomId] = evId;
|
this.readUpToMap[roomId] = evId;
|
||||||
|
|
@ -402,6 +414,7 @@ export class CallEmbed {
|
||||||
} else {
|
} else {
|
||||||
const raw = ev.getEffectiveEvent();
|
const raw = ev.getEffectiveEvent();
|
||||||
this.call.feedEvent(raw as IRoomEvent).catch((e) => {
|
this.call.feedEvent(raw as IRoomEvent).catch((e) => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.error('Error sending event to widget: ', e);
|
console.error('Error sending event to widget: ', e);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -149,7 +149,10 @@ export const renderMatrixMention = (
|
||||||
|
|
||||||
export const factoryRenderLinkifyWithMention = (
|
export const factoryRenderLinkifyWithMention = (
|
||||||
mentionRender: (href: string) => JSX.Element | undefined
|
mentionRender: (href: string) => JSX.Element | undefined
|
||||||
|
// linkifyjs render callback is typed `(ir: IntermediateRepresentation) => any` upstream.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
): OptFn<(ir: IntermediateRepresentation) => any> => {
|
): OptFn<(ir: IntermediateRepresentation) => any> => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const render: OptFn<(ir: IntermediateRepresentation) => any> = ({
|
const render: OptFn<(ir: IntermediateRepresentation) => any> = ({
|
||||||
tagName,
|
tagName,
|
||||||
attributes,
|
attributes,
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,9 @@ export class ASCIILexicalTable {
|
||||||
this.populateWidthToSize();
|
this.populateWidthToSize();
|
||||||
|
|
||||||
if (this.size() > Number.MAX_SAFE_INTEGER) {
|
if (this.size() > Number.MAX_SAFE_INTEGER) {
|
||||||
|
// Diagnostic for upstream Slate decorator math overflow — extremely
|
||||||
|
// unlikely to fire on real alphabets but worth surfacing to devs.
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.warn(
|
console.warn(
|
||||||
`[!] Warning: ASCIILexicalTable size is larger than the Number.MAX_SAFE_INTEGER: ${this.size()} > ${
|
`[!] Warning: ASCIILexicalTable size is larger than the Number.MAX_SAFE_INTEGER: ${this.size()} > ${
|
||||||
Number.MAX_SAFE_INTEGER
|
Number.MAX_SAFE_INTEGER
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ export const promiseFulfilledResult = <T>(
|
||||||
if (settledResult.status === 'fulfilled') return settledResult.value;
|
if (settledResult.status === 'fulfilled') return settledResult.value;
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
export const promiseRejectedResult = <T>(settledResult: PromiseSettledResult<T>): any => {
|
export const promiseRejectedResult = <T>(settledResult: PromiseSettledResult<T>): unknown => {
|
||||||
if (settledResult.status === 'rejected') return settledResult.reason;
|
if (settledResult.status === 'rejected') return settledResult.reason;
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,10 @@ export const getLoginTermUrl = (params: UIAParams): string | undefined => {
|
||||||
if (terms.policies === null) return undefined;
|
if (terms.policies === null) return undefined;
|
||||||
if ('privacy_policy' in terms.policies && typeof terms.policies.privacy_policy === 'object') {
|
if ('privacy_policy' in terms.policies && typeof terms.policies.privacy_policy === 'object') {
|
||||||
if (terms.policies.privacy_policy === null) return undefined;
|
if (terms.policies.privacy_policy === null) return undefined;
|
||||||
const langToPolicy = terms.policies.privacy_policy as Record<string, any>;
|
const langToPolicy = terms.policies.privacy_policy as Record<
|
||||||
|
string,
|
||||||
|
{ url?: string } | undefined
|
||||||
|
>;
|
||||||
const url = langToPolicy.en?.url;
|
const url = langToPolicy.en?.url;
|
||||||
if (typeof url === 'string') return url;
|
if (typeof url === 'string') return url;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
UploadProgress,
|
UploadProgress,
|
||||||
UploadResponse,
|
UploadResponse,
|
||||||
} from 'matrix-js-sdk';
|
} from 'matrix-js-sdk';
|
||||||
|
import type { AccountDataEvents } from 'matrix-js-sdk';
|
||||||
import to from 'await-to-js';
|
import to from 'await-to-js';
|
||||||
import { IImageInfo, IThumbnailContent, IVideoInfo } from '../../types/matrix/common';
|
import { IImageInfo, IThumbnailContent, IVideoInfo } from '../../types/matrix/common';
|
||||||
import { AccountDataEvent } from '../../types/matrix/accountData';
|
import { AccountDataEvent } from '../../types/matrix/accountData';
|
||||||
|
|
@ -164,9 +165,10 @@ export const uploadContent = async (
|
||||||
const mxc = data.content_uri;
|
const mxc = data.content_uri;
|
||||||
if (mxc) onSuccess(mxc);
|
if (mxc) onSuccess(mxc);
|
||||||
else onError(new MatrixError(data));
|
else onError(new MatrixError(data));
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
const error = typeof e?.message === 'string' ? e.message : undefined;
|
const err = e as { message?: unknown; name?: unknown };
|
||||||
const errcode = typeof e?.name === 'string' ? e.message : undefined;
|
const error = typeof err?.message === 'string' ? err.message : undefined;
|
||||||
|
const errcode = typeof err?.name === 'string' ? err.name : undefined;
|
||||||
onError(new MatrixError({ error, errcode }));
|
onError(new MatrixError({ error, errcode }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -184,7 +186,7 @@ export const eventWithShortcode = (ev: MatrixEvent) =>
|
||||||
// Element's findDMForUser (matrix-react-sdk/src/utils/dm/findDMForUser.ts)
|
// Element's findDMForUser (matrix-react-sdk/src/utils/dm/findDMForUser.ts)
|
||||||
// including the cross-user fallback scan for stale peer state (PR #10127).
|
// including the cross-user fallback scan for stale peer state (PR #10127).
|
||||||
export const getDMRoomFor = (mx: MatrixClient, userId: string): Room | undefined => {
|
export const getDMRoomFor = (mx: MatrixClient, userId: string): Room | undefined => {
|
||||||
const mDirectsEvent = mx.getAccountData(AccountDataEvent.Direct as any);
|
const mDirectsEvent = mx.getAccountData(AccountDataEvent.Direct as keyof AccountDataEvents);
|
||||||
const mDirect = (mDirectsEvent?.getContent() ?? {}) as Record<string, string[]>;
|
const mDirect = (mDirectsEvent?.getContent() ?? {}) as Record<string, string[]>;
|
||||||
|
|
||||||
const myUserId = mx.getUserId();
|
const myUserId = mx.getUserId();
|
||||||
|
|
@ -264,7 +266,7 @@ export const addRoomIdToMDirect = (
|
||||||
userId: string
|
userId: string
|
||||||
): Promise<void> =>
|
): Promise<void> =>
|
||||||
enqueueMDirectWrite(mx, async () => {
|
enqueueMDirectWrite(mx, async () => {
|
||||||
const mDirectsEvent = mx.getAccountData(AccountDataEvent.Direct as any);
|
const mDirectsEvent = mx.getAccountData(AccountDataEvent.Direct as keyof AccountDataEvents);
|
||||||
let userIdToRoomIds: Record<string, string[]> = {};
|
let userIdToRoomIds: Record<string, string[]> = {};
|
||||||
|
|
||||||
if (typeof mDirectsEvent !== 'undefined')
|
if (typeof mDirectsEvent !== 'undefined')
|
||||||
|
|
@ -287,12 +289,12 @@ export const addRoomIdToMDirect = (
|
||||||
}
|
}
|
||||||
userIdToRoomIds[userId] = roomIds;
|
userIdToRoomIds[userId] = roomIds;
|
||||||
|
|
||||||
await mx.setAccountData(AccountDataEvent.Direct as any, userIdToRoomIds as any);
|
await mx.setAccountData(AccountDataEvent.Direct as keyof AccountDataEvents, userIdToRoomIds);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const removeRoomIdFromMDirect = (mx: MatrixClient, roomId: string): Promise<void> =>
|
export const removeRoomIdFromMDirect = (mx: MatrixClient, roomId: string): Promise<void> =>
|
||||||
enqueueMDirectWrite(mx, async () => {
|
enqueueMDirectWrite(mx, async () => {
|
||||||
const mDirectsEvent = mx.getAccountData(AccountDataEvent.Direct as any);
|
const mDirectsEvent = mx.getAccountData(AccountDataEvent.Direct as keyof AccountDataEvents);
|
||||||
let userIdToRoomIds: Record<string, string[]> = {};
|
let userIdToRoomIds: Record<string, string[]> = {};
|
||||||
|
|
||||||
if (typeof mDirectsEvent !== 'undefined')
|
if (typeof mDirectsEvent !== 'undefined')
|
||||||
|
|
@ -306,7 +308,7 @@ export const removeRoomIdFromMDirect = (mx: MatrixClient, roomId: string): Promi
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await mx.setAccountData(AccountDataEvent.Direct as any, userIdToRoomIds as any);
|
await mx.setAccountData(AccountDataEvent.Direct as keyof AccountDataEvents, userIdToRoomIds);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const mxcUrlToHttp = (
|
export const mxcUrlToHttp = (
|
||||||
|
|
|
||||||
|
|
@ -141,7 +141,6 @@ export async function ensureRtcRingPushRule(mx: MatrixClient): Promise<void> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Symmetric cleanup for `useDisablePushNotifications`. The rules live in
|
// Symmetric cleanup for `useDisablePushNotifications`. The rules live in
|
||||||
// account data so they would otherwise outlive the pusher — and other logged-in
|
// account data so they would otherwise outlive the pusher — and other logged-in
|
||||||
// clients (Element, a second Vojo session) would keep applying the ring
|
// clients (Element, a second Vojo session) would keep applying the ring
|
||||||
|
|
@ -163,7 +162,6 @@ export async function removeRtcRingPushRule(mx: MatrixClient): Promise<void> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function registerWebPusher(
|
export async function registerWebPusher(
|
||||||
mx: MatrixClient,
|
mx: MatrixClient,
|
||||||
subscription: PushSubscription,
|
subscription: PushSubscription,
|
||||||
|
|
@ -193,13 +191,17 @@ export async function registerWebPusher(
|
||||||
app_display_name: 'Vojo Web',
|
app_display_name: 'Vojo Web',
|
||||||
device_display_name: navigator.userAgent.slice(0, 120),
|
device_display_name: navigator.userAgent.slice(0, 120),
|
||||||
lang: navigator.language || 'en',
|
lang: navigator.language || 'en',
|
||||||
|
// SDK's pusher `data` type only declares `{ format?, url?, brand? }`.
|
||||||
|
// Sygnal / UnifiedPush also read `endpoint`, `auth`, `events_only`; the
|
||||||
|
// extra fields are forwarded to the gateway and required for web-push
|
||||||
|
// delivery. Cast through `unknown` to drop the over-narrow SDK shape.
|
||||||
data: {
|
data: {
|
||||||
url: gatewayUrl,
|
url: gatewayUrl,
|
||||||
endpoint: subscription.endpoint,
|
endpoint: subscription.endpoint,
|
||||||
auth: arrayBufferToBase64Url(auth),
|
auth: arrayBufferToBase64Url(auth),
|
||||||
format: 'event_id_only',
|
format: 'event_id_only',
|
||||||
events_only: true,
|
events_only: true,
|
||||||
},
|
} as unknown as { format?: string; url?: string; brand?: string },
|
||||||
append: true,
|
append: true,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import {
|
||||||
Room,
|
Room,
|
||||||
RoomMember,
|
RoomMember,
|
||||||
} from 'matrix-js-sdk';
|
} from 'matrix-js-sdk';
|
||||||
|
import type { AccountDataEvents } from 'matrix-js-sdk';
|
||||||
import { CryptoBackend } from 'matrix-js-sdk/lib/common-crypto/CryptoBackend';
|
import { CryptoBackend } from 'matrix-js-sdk/lib/common-crypto/CryptoBackend';
|
||||||
import { AccountDataEvent } from '../../types/matrix/accountData';
|
import { AccountDataEvent } from '../../types/matrix/accountData';
|
||||||
import {
|
import {
|
||||||
|
|
@ -44,7 +45,7 @@ export const getStateEvents = (room: Room, eventType: StateEvent): MatrixEvent[]
|
||||||
export const getAccountData = (
|
export const getAccountData = (
|
||||||
mx: MatrixClient,
|
mx: MatrixClient,
|
||||||
eventType: AccountDataEvent
|
eventType: AccountDataEvent
|
||||||
): MatrixEvent | undefined => mx.getAccountData(eventType as any);
|
): MatrixEvent | undefined => mx.getAccountData(eventType as keyof AccountDataEvents);
|
||||||
|
|
||||||
export const getMDirects = (mDirectEvent: MatrixEvent): Set<string> => {
|
export const getMDirects = (mDirectEvent: MatrixEvent): Set<string> => {
|
||||||
const roomIds = new Set<string>();
|
const roomIds = new Set<string>();
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@ export const initClient = async (session: Session): Promise<MatrixClient> => {
|
||||||
cryptoStore: legacyCryptoStore,
|
cryptoStore: legacyCryptoStore,
|
||||||
deviceId: session.deviceId,
|
deviceId: session.deviceId,
|
||||||
timelineSupport: true,
|
timelineSupport: true,
|
||||||
|
// cryptoCallbacks ships from a .js module with a duck-typed shape; matrix-js-sdk's exported type is too narrow.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
cryptoCallbacks: cryptoCallbacks as any,
|
cryptoCallbacks: cryptoCallbacks as any,
|
||||||
verificationMethods: ['m.sas.v1'],
|
verificationMethods: ['m.sas.v1'],
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,9 @@ const mountApp = () => {
|
||||||
const rootContainer = document.getElementById('root');
|
const rootContainer = document.getElementById('root');
|
||||||
|
|
||||||
if (rootContainer === null) {
|
if (rootContainer === null) {
|
||||||
|
// Bootstrap failure — only surfaces if `index.html` is broken; logging
|
||||||
|
// is the only path here since React hasn't mounted yet.
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.error('Root container element not found!');
|
console.error('Root container element not found!');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ async function cleanupDeadClients() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setSession(clientId: string, accessToken: any, baseUrl: any) {
|
function setSession(clientId: string, accessToken: unknown, baseUrl: unknown) {
|
||||||
if (typeof accessToken === 'string' && typeof baseUrl === 'string') {
|
if (typeof accessToken === 'string' && typeof baseUrl === 'string') {
|
||||||
sessions.set(clientId, { accessToken, baseUrl });
|
sessions.set(clientId, { accessToken, baseUrl });
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
33
src/types/matrix/sdkAugmentation.d.ts
vendored
Normal file
33
src/types/matrix/sdkAugmentation.d.ts
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Module augmentation: extend matrix-js-sdk's AccountDataEvents and StateEvents
|
||||||
|
// interfaces with the custom Matrix event types Vojo / upstream Cinny uses
|
||||||
|
// (Vojo spaces sidebar, Ponies emote/sticker packs, Element recent emoji,
|
||||||
|
// MSC2346 bridge metadata, MSC3401 group-call membership, MSC4143 RTC member,
|
||||||
|
// Vojo power-level tags). Without this, `mx.getAccountData(<custom-key>)` /
|
||||||
|
// `mx.getStateEvent(<custom-key>)` / `state.getStateEvents(<custom-key>)` fail
|
||||||
|
// `keyof XxxEvents` narrowing in TypeScript 5.4+.
|
||||||
|
//
|
||||||
|
// Content types stay `unknown` here on purpose — call sites already invoke
|
||||||
|
// `.getContent<T>()` with the right local content type. Keep this file dep-free
|
||||||
|
// (only ambient declarations) so it loads before any value imports.
|
||||||
|
|
||||||
|
import 'matrix-js-sdk';
|
||||||
|
|
||||||
|
declare module 'matrix-js-sdk' {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface, @typescript-eslint/consistent-type-definitions
|
||||||
|
interface AccountDataEvents {
|
||||||
|
'in.vojo.spaces': unknown;
|
||||||
|
'io.element.recent_emoji': unknown;
|
||||||
|
'im.ponies.user_emotes': unknown;
|
||||||
|
'im.ponies.emote_rooms': unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface, @typescript-eslint/consistent-type-definitions
|
||||||
|
interface StateEvents {
|
||||||
|
'im.ponies.room_emotes': unknown;
|
||||||
|
'in.vojo.room.power_level_tags': unknown;
|
||||||
|
// MSC2346 bridge metadata — stable + unstable variants. Mautrix-* and
|
||||||
|
// similar bridges write at least one; SDK ships no built-in typing.
|
||||||
|
'm.bridge': unknown;
|
||||||
|
'uk.half-shot.bridge': unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue