From 87ce209050472d51961e1a403dbd63ec0043e5d5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Jun 2025 08:29:55 +1000 Subject: [PATCH 01/63] Bump actions/setup-node from 4.3.0 to 4.4.0 (#2307) Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.3.0 to 4.4.0. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v4.3.0...v4.4.0) --- updated-dependencies: - dependency-name: actions/setup-node dependency-version: 4.4.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-pull-request.yml | 2 +- .github/workflows/netlify-dev.yml | 2 +- .github/workflows/prod-deploy.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-pull-request.yml b/.github/workflows/build-pull-request.yml index 441da0de..450e4e29 100644 --- a/.github/workflows/build-pull-request.yml +++ b/.github/workflows/build-pull-request.yml @@ -14,7 +14,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v4.2.0 - name: Setup node - uses: actions/setup-node@v4.3.0 + uses: actions/setup-node@v4.4.0 with: node-version: 20.12.2 cache: 'npm' diff --git a/.github/workflows/netlify-dev.yml b/.github/workflows/netlify-dev.yml index 34308c21..66cd5ad5 100644 --- a/.github/workflows/netlify-dev.yml +++ b/.github/workflows/netlify-dev.yml @@ -13,7 +13,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v4.2.0 - name: Setup node - uses: actions/setup-node@v4.3.0 + uses: actions/setup-node@v4.4.0 with: node-version: 20.12.2 cache: 'npm' diff --git a/.github/workflows/prod-deploy.yml b/.github/workflows/prod-deploy.yml index 44205ff2..b11da5be 100644 --- a/.github/workflows/prod-deploy.yml +++ b/.github/workflows/prod-deploy.yml @@ -12,7 +12,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v4.2.0 - name: Setup node - uses: actions/setup-node@v4.3.0 + uses: actions/setup-node@v4.4.0 with: node-version: 20.12.2 cache: 'npm' From ba72925d53dac5e4a06d85dfaef68ee2c8b1caf6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Jun 2025 08:52:03 +1000 Subject: [PATCH 02/63] Bump docker/build-push-action from 6.15.0 to 6.18.0 (#2351) Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.15.0 to 6.18.0. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/v6.15.0...v6.18.0) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-version: 6.18.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker-pr.yml | 2 +- .github/workflows/prod-deploy.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-pr.yml b/.github/workflows/docker-pr.yml index 4e88c78d..398785ab 100644 --- a/.github/workflows/docker-pr.yml +++ b/.github/workflows/docker-pr.yml @@ -13,7 +13,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v4.2.0 - name: Build Docker image - uses: docker/build-push-action@v6.15.0 + uses: docker/build-push-action@v6.18.0 with: context: . push: false diff --git a/.github/workflows/prod-deploy.yml b/.github/workflows/prod-deploy.yml index b11da5be..0a758c51 100644 --- a/.github/workflows/prod-deploy.yml +++ b/.github/workflows/prod-deploy.yml @@ -90,7 +90,7 @@ jobs: ${{ secrets.DOCKER_USERNAME }}/cinny ghcr.io/${{ github.repository }} - name: Build and push Docker image - uses: docker/build-push-action@v6.15.0 + uses: docker/build-push-action@v6.18.0 with: context: . platforms: linux/amd64,linux/arm64 From 05e83eabef9b72ad36849f29d62ce8ac103a8308 Mon Sep 17 00:00:00 2001 From: Priyansh <157942154+Priyansh1547@users.noreply.github.com> Date: Fri, 27 Jun 2025 21:50:28 +0530 Subject: [PATCH 03/63] Fix auto focus in "Join with Address" text input (#2317) --- src/app/organisms/join-alias/JoinAlias.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/organisms/join-alias/JoinAlias.jsx b/src/app/organisms/join-alias/JoinAlias.jsx index 99cf6e6e..d4e313af 100644 --- a/src/app/organisms/join-alias/JoinAlias.jsx +++ b/src/app/organisms/join-alias/JoinAlias.jsx @@ -75,7 +75,7 @@ function JoinAliasContent({ term, requestClose }) { return (
- + {error && ( {error} From 461e730c345370fe9afa422127c9b112fef31288 Mon Sep 17 00:00:00 2001 From: Gimle Larpes <97182804+GimleLarpes@users.noreply.github.com> Date: Sat, 28 Jun 2025 12:35:59 +0200 Subject: [PATCH 04/63] Make "View Source" a developer tool (#2368) --- src/app/features/room/RoomTimeline.tsx | 10 ++++++++++ src/app/features/room/message/Message.tsx | 20 ++++++++++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/app/features/room/RoomTimeline.tsx b/src/app/features/room/RoomTimeline.tsx index 05caf4b0..773e115b 100644 --- a/src/app/features/room/RoomTimeline.tsx +++ b/src/app/features/room/RoomTimeline.tsx @@ -448,6 +448,7 @@ export function RoomTimeline({ const [encUrlPreview] = useSetting(settingsAtom, 'encUrlPreview'); const showUrlPreview = room.hasEncryptionStateEvent() ? encUrlPreview : urlPreview; const [showHiddenEvents] = useSetting(settingsAtom, 'showHiddenEvents'); + const [showDeveloperTools] = useSetting(settingsAtom, 'developerTools'); const ignoredUsersList = useIgnoredUsers(); const ignoredUsersSet = useMemo(() => new Set(ignoredUsersList), [ignoredUsersList]); @@ -1065,6 +1066,7 @@ export function RoomTimeline({ ) } hideReadReceipts={hideActivity} + showDeveloperTools={showDeveloperTools} powerLevelTag={getPowerLevelTag(senderPowerLevel)} accessibleTagColors={accessibleTagColors} legacyUsernameColor={legacyUsernameColor || direct} @@ -1146,6 +1148,7 @@ export function RoomTimeline({ ) } hideReadReceipts={hideActivity} + showDeveloperTools={showDeveloperTools} powerLevelTag={getPowerLevelTag(senderPowerLevel)} accessibleTagColors={accessibleTagColors} legacyUsernameColor={legacyUsernameColor || direct} @@ -1247,6 +1250,7 @@ export function RoomTimeline({ ) } hideReadReceipts={hideActivity} + showDeveloperTools={showDeveloperTools} powerLevelTag={getPowerLevelTag(senderPowerLevel)} accessibleTagColors={accessibleTagColors} legacyUsernameColor={legacyUsernameColor || direct} @@ -1292,6 +1296,7 @@ export function RoomTimeline({ messageSpacing={messageSpacing} canDelete={canRedact || mEvent.getSender() === mx.getUserId()} hideReadReceipts={hideActivity} + showDeveloperTools={showDeveloperTools} > ; legacyUsernameColor?: boolean; @@ -703,6 +704,7 @@ export const Message = as<'div', MessageProps>( reply, reactions, hideReadReceipts, + showDeveloperTools, powerLevelTag, accessibleTagColors, legacyUsernameColor, @@ -1026,7 +1028,13 @@ export const Message = as<'div', MessageProps>( onClose={closeMenu} /> )} - + {showDeveloperTools && ( + + )} {canPinEvent && ( @@ -1101,6 +1109,7 @@ export type EventProps = { canDelete?: boolean; messageSpacing: MessageSpacing; hideReadReceipts?: boolean; + showDeveloperTools?: boolean; }; export const Event = as<'div', EventProps>( ( @@ -1112,6 +1121,7 @@ export const Event = as<'div', EventProps>( canDelete, messageSpacing, hideReadReceipts, + showDeveloperTools, children, ...props }, @@ -1188,7 +1198,13 @@ export const Event = as<'div', EventProps>( onClose={closeMenu} /> )} - + {showDeveloperTools && ( + + )} {((!mEvent.isRedacted() && canDelete && !stateEvent) || From 77ab37f637c3f7d597c4ae3660fc5dedaa6a3e71 Mon Sep 17 00:00:00 2001 From: Gimle Larpes <97182804+GimleLarpes@users.noreply.github.com> Date: Sat, 28 Jun 2025 16:45:21 +0200 Subject: [PATCH 05/63] Fix focus behaviour when opening single-purpose features (#2349) * Improve focus behaviour on search boxes and chats * Implemented MR #2317 * Fix crash if canMessage is false * Prepare for PR #2335 * disable autofocus on message field --- src/app/features/message-search/SearchInput.tsx | 1 + src/app/organisms/invite-user/InviteUser.jsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/features/message-search/SearchInput.tsx b/src/app/features/message-search/SearchInput.tsx index db646c26..533eb5fd 100644 --- a/src/app/features/message-search/SearchInput.tsx +++ b/src/app/features/message-search/SearchInput.tsx @@ -29,6 +29,7 @@ export function SearchInput({ active, loading, searchInputRef, onSearch, onReset ref={searchInputRef} style={{ paddingRight: config.space.S300 }} name="searchInput" + autoFocus size="500" variant="Background" placeholder="Search for keyword" diff --git a/src/app/organisms/invite-user/InviteUser.jsx b/src/app/organisms/invite-user/InviteUser.jsx index c5bade69..271c22a9 100644 --- a/src/app/organisms/invite-user/InviteUser.jsx +++ b/src/app/organisms/invite-user/InviteUser.jsx @@ -273,7 +273,7 @@ function InviteUser({ isOpen, roomId, searchTerm, onRequestClose }) { searchUser(usernameRef.current.value); }} > - + From ebe5beba1de7aab9b8bee50438d407d07bd37e6f Mon Sep 17 00:00:00 2001 From: RGBCube Date: Sun, 29 Jun 2025 10:43:47 +0000 Subject: [PATCH 06/63] Add support for more code highlight (#2355) --- src/app/plugins/react-prism/ReactPrism.tsx | 313 ++++++++++++++++++++- 1 file changed, 301 insertions(+), 12 deletions(-) diff --git a/src/app/plugins/react-prism/ReactPrism.tsx b/src/app/plugins/react-prism/ReactPrism.tsx index f93c6ef1..ab2e9320 100644 --- a/src/app/plugins/react-prism/ReactPrism.tsx +++ b/src/app/plugins/react-prism/ReactPrism.tsx @@ -2,18 +2,307 @@ import React, { MutableRefObject, ReactNode, useEffect, useRef } from 'react'; import Prism from 'prismjs'; -import 'prismjs/components/prism-json'; -import 'prismjs/components/prism-javascript'; -import 'prismjs/components/prism-typescript'; -import 'prismjs/components/prism-css'; -import 'prismjs/components/prism-sass'; -import 'prismjs/components/prism-swift'; -import 'prismjs/components/prism-rust'; -import 'prismjs/components/prism-go'; -import 'prismjs/components/prism-c'; -import 'prismjs/components/prism-cpp'; -import 'prismjs/components/prism-java'; -import 'prismjs/components/prism-python'; +import 'prismjs/components/prism-abap.js'; +import 'prismjs/components/prism-abnf.js'; +import 'prismjs/components/prism-actionscript.js'; +import 'prismjs/components/prism-ada.js'; +import 'prismjs/components/prism-agda.js'; +import 'prismjs/components/prism-al.js'; +import 'prismjs/components/prism-antlr4.js'; +import 'prismjs/components/prism-apacheconf.js'; +import 'prismjs/components/prism-apex.js'; +import 'prismjs/components/prism-apl.js'; +import 'prismjs/components/prism-applescript.js'; +import 'prismjs/components/prism-aql.js'; +import 'prismjs/components/prism-arff.js'; +import 'prismjs/components/prism-armasm.js'; +import 'prismjs/components/prism-arturo.js'; +import 'prismjs/components/prism-asciidoc.js'; +import 'prismjs/components/prism-asm6502.js'; +import 'prismjs/components/prism-asmatmel.js'; +import 'prismjs/components/prism-aspnet.js'; +import 'prismjs/components/prism-autohotkey.js'; +import 'prismjs/components/prism-autoit.js'; +import 'prismjs/components/prism-avisynth.js'; +import 'prismjs/components/prism-avro-idl.js'; +import 'prismjs/components/prism-awk.js'; +import 'prismjs/components/prism-bash.js'; +import 'prismjs/components/prism-basic.js'; +import 'prismjs/components/prism-batch.js'; +import 'prismjs/components/prism-bbcode.js'; +import 'prismjs/components/prism-bbj.js'; +import 'prismjs/components/prism-bicep.js'; +import 'prismjs/components/prism-birb.js'; +import 'prismjs/components/prism-bnf.js'; +import 'prismjs/components/prism-bqn.js'; +import 'prismjs/components/prism-brainfuck.js'; +import 'prismjs/components/prism-brightscript.js'; +import 'prismjs/components/prism-bro.js'; +import 'prismjs/components/prism-bsl.js'; +import 'prismjs/components/prism-c.js'; +import 'prismjs/components/prism-cfscript.js'; +import 'prismjs/components/prism-cil.js'; +import 'prismjs/components/prism-cilkc.js'; +import 'prismjs/components/prism-cilkcpp.js'; +import 'prismjs/components/prism-clike.js'; +import 'prismjs/components/prism-clojure.js'; +import 'prismjs/components/prism-cmake.js'; +import 'prismjs/components/prism-cobol.js'; +import 'prismjs/components/prism-coffeescript.js'; +import 'prismjs/components/prism-concurnas.js'; +import 'prismjs/components/prism-cooklang.js'; +import 'prismjs/components/prism-coq.js'; +import 'prismjs/components/prism-cpp.js'; +import 'prismjs/components/prism-csharp.js'; +import 'prismjs/components/prism-cshtml.js'; +import 'prismjs/components/prism-csp.js'; +import 'prismjs/components/prism-css-extras.js'; +import 'prismjs/components/prism-css.js'; +import 'prismjs/components/prism-csv.js'; +import 'prismjs/components/prism-cue.js'; +import 'prismjs/components/prism-cypher.js'; +import 'prismjs/components/prism-d.js'; +import 'prismjs/components/prism-dart.js'; +import 'prismjs/components/prism-dataweave.js'; +import 'prismjs/components/prism-dax.js'; +import 'prismjs/components/prism-dhall.js'; +import 'prismjs/components/prism-diff.js'; +import 'prismjs/components/prism-dns-zone-file.js'; +import 'prismjs/components/prism-docker.js'; +import 'prismjs/components/prism-dot.js'; +import 'prismjs/components/prism-ebnf.js'; +import 'prismjs/components/prism-editorconfig.js'; +import 'prismjs/components/prism-eiffel.js'; +import 'prismjs/components/prism-ejs.js'; +import 'prismjs/components/prism-elixir.js'; +import 'prismjs/components/prism-elm.js'; +import 'prismjs/components/prism-erb.js'; +import 'prismjs/components/prism-erlang.js'; +import 'prismjs/components/prism-etlua.js'; +import 'prismjs/components/prism-excel-formula.js'; +import 'prismjs/components/prism-factor.js'; +import 'prismjs/components/prism-false.js'; +import 'prismjs/components/prism-firestore-security-rules.js'; +import 'prismjs/components/prism-flow.js'; +import 'prismjs/components/prism-fortran.js'; +import 'prismjs/components/prism-fsharp.js'; +import 'prismjs/components/prism-ftl.js'; +import 'prismjs/components/prism-gap.js'; +import 'prismjs/components/prism-gcode.js'; +import 'prismjs/components/prism-gdscript.js'; +import 'prismjs/components/prism-gedcom.js'; +import 'prismjs/components/prism-gettext.js'; +import 'prismjs/components/prism-gherkin.js'; +import 'prismjs/components/prism-git.js'; +import 'prismjs/components/prism-glsl.js'; +import 'prismjs/components/prism-gml.js'; +import 'prismjs/components/prism-gn.js'; +import 'prismjs/components/prism-go-module.js'; +import 'prismjs/components/prism-go.js'; +import 'prismjs/components/prism-gradle.js'; +import 'prismjs/components/prism-graphql.js'; +import 'prismjs/components/prism-groovy.js'; +import 'prismjs/components/prism-haml.js'; +import 'prismjs/components/prism-handlebars.js'; +import 'prismjs/components/prism-haskell.js'; +import 'prismjs/components/prism-haxe.js'; +import 'prismjs/components/prism-hcl.js'; +import 'prismjs/components/prism-hlsl.js'; +import 'prismjs/components/prism-hoon.js'; +import 'prismjs/components/prism-hpkp.js'; +import 'prismjs/components/prism-hsts.js'; +import 'prismjs/components/prism-http.js'; +import 'prismjs/components/prism-ichigojam.js'; +import 'prismjs/components/prism-icon.js'; +import 'prismjs/components/prism-icu-message-format.js'; +import 'prismjs/components/prism-idris.js'; +import 'prismjs/components/prism-iecst.js'; +import 'prismjs/components/prism-ignore.js'; +import 'prismjs/components/prism-inform7.js'; +import 'prismjs/components/prism-ini.js'; +import 'prismjs/components/prism-io.js'; +import 'prismjs/components/prism-j.js'; +import 'prismjs/components/prism-java.js'; +import 'prismjs/components/prism-javadoclike.js'; +import 'prismjs/components/prism-javascript.js'; +import 'prismjs/components/prism-javastacktrace.js'; +import 'prismjs/components/prism-jexl.js'; +import 'prismjs/components/prism-jolie.js'; +import 'prismjs/components/prism-jq.js'; +import 'prismjs/components/prism-js-extras.js'; +import 'prismjs/components/prism-js-templates.js'; +import 'prismjs/components/prism-json.js'; +import 'prismjs/components/prism-json5.js'; +import 'prismjs/components/prism-jsonp.js'; +import 'prismjs/components/prism-jsstacktrace.js'; +import 'prismjs/components/prism-jsx.js'; +import 'prismjs/components/prism-julia.js'; +import 'prismjs/components/prism-keepalived.js'; +import 'prismjs/components/prism-keyman.js'; +import 'prismjs/components/prism-kotlin.js'; +import 'prismjs/components/prism-kumir.js'; +import 'prismjs/components/prism-kusto.js'; +import 'prismjs/components/prism-latex.js'; +import 'prismjs/components/prism-latte.js'; +import 'prismjs/components/prism-less.js'; +import 'prismjs/components/prism-lilypond.js'; +import 'prismjs/components/prism-linker-script.js'; +import 'prismjs/components/prism-liquid.js'; +import 'prismjs/components/prism-lisp.js'; +import 'prismjs/components/prism-livescript.js'; +import 'prismjs/components/prism-llvm.js'; +import 'prismjs/components/prism-log.js'; +import 'prismjs/components/prism-lolcode.js'; +import 'prismjs/components/prism-lua.js'; +import 'prismjs/components/prism-magma.js'; +import 'prismjs/components/prism-makefile.js'; +import 'prismjs/components/prism-markdown.js'; +import 'prismjs/components/prism-markup-templating.js'; +import 'prismjs/components/prism-markup.js'; +import 'prismjs/components/prism-mata.js'; +import 'prismjs/components/prism-matlab.js'; +import 'prismjs/components/prism-maxscript.js'; +import 'prismjs/components/prism-mel.js'; +import 'prismjs/components/prism-mermaid.js'; +import 'prismjs/components/prism-metafont.js'; +import 'prismjs/components/prism-mizar.js'; +import 'prismjs/components/prism-mongodb.js'; +import 'prismjs/components/prism-monkey.js'; +import 'prismjs/components/prism-moonscript.js'; +import 'prismjs/components/prism-n1ql.js'; +import 'prismjs/components/prism-n4js.js'; +import 'prismjs/components/prism-nand2tetris-hdl.js'; +import 'prismjs/components/prism-naniscript.js'; +import 'prismjs/components/prism-nasm.js'; +import 'prismjs/components/prism-neon.js'; +import 'prismjs/components/prism-nevod.js'; +import 'prismjs/components/prism-nginx.js'; +import 'prismjs/components/prism-nim.js'; +import 'prismjs/components/prism-nix.js'; +import 'prismjs/components/prism-nsis.js'; +import 'prismjs/components/prism-objectivec.js'; +import 'prismjs/components/prism-ocaml.js'; +import 'prismjs/components/prism-odin.js'; +import 'prismjs/components/prism-opencl.js'; +import 'prismjs/components/prism-openqasm.js'; +import 'prismjs/components/prism-oz.js'; +import 'prismjs/components/prism-parigp.js'; +import 'prismjs/components/prism-parser.js'; +import 'prismjs/components/prism-pascal.js'; +import 'prismjs/components/prism-pascaligo.js'; +import 'prismjs/components/prism-pcaxis.js'; +import 'prismjs/components/prism-peoplecode.js'; +import 'prismjs/components/prism-perl.js'; +import 'prismjs/components/prism-php-extras.js'; +import 'prismjs/components/prism-php.js'; +import 'prismjs/components/prism-phpdoc.js'; +import 'prismjs/components/prism-plant-uml.js'; +import 'prismjs/components/prism-powerquery.js'; +import 'prismjs/components/prism-powershell.js'; +import 'prismjs/components/prism-processing.js'; +import 'prismjs/components/prism-prolog.js'; +import 'prismjs/components/prism-promql.js'; +import 'prismjs/components/prism-properties.js'; +import 'prismjs/components/prism-protobuf.js'; +import 'prismjs/components/prism-psl.js'; +import 'prismjs/components/prism-pug.js'; +import 'prismjs/components/prism-puppet.js'; +import 'prismjs/components/prism-pure.js'; +import 'prismjs/components/prism-purebasic.js'; +import 'prismjs/components/prism-purescript.js'; +import 'prismjs/components/prism-python.js'; +import 'prismjs/components/prism-q.js'; +import 'prismjs/components/prism-qml.js'; +import 'prismjs/components/prism-qore.js'; +import 'prismjs/components/prism-qsharp.js'; +import 'prismjs/components/prism-r.js'; +import 'prismjs/components/prism-reason.js'; +import 'prismjs/components/prism-regex.js'; +import 'prismjs/components/prism-rego.js'; +import 'prismjs/components/prism-renpy.js'; +import 'prismjs/components/prism-rescript.js'; +import 'prismjs/components/prism-rest.js'; +import 'prismjs/components/prism-rip.js'; +import 'prismjs/components/prism-roboconf.js'; +import 'prismjs/components/prism-robotframework.js'; +import 'prismjs/components/prism-ruby.js'; +import 'prismjs/components/prism-rust.js'; +import 'prismjs/components/prism-sas.js'; +import 'prismjs/components/prism-sass.js'; +import 'prismjs/components/prism-scala.js'; +import 'prismjs/components/prism-scheme.js'; +import 'prismjs/components/prism-scss.js'; +import 'prismjs/components/prism-shell-session.js'; +import 'prismjs/components/prism-smali.js'; +import 'prismjs/components/prism-smalltalk.js'; +import 'prismjs/components/prism-smarty.js'; +import 'prismjs/components/prism-sml.js'; +import 'prismjs/components/prism-solidity.js'; +import 'prismjs/components/prism-solution-file.js'; +import 'prismjs/components/prism-soy.js'; +import 'prismjs/components/prism-splunk-spl.js'; +import 'prismjs/components/prism-sqf.js'; +import 'prismjs/components/prism-sql.js'; +import 'prismjs/components/prism-squirrel.js'; +import 'prismjs/components/prism-stan.js'; +import 'prismjs/components/prism-stata.js'; +import 'prismjs/components/prism-stylus.js'; +import 'prismjs/components/prism-supercollider.js'; +import 'prismjs/components/prism-swift.js'; +import 'prismjs/components/prism-systemd.js'; +import 'prismjs/components/prism-t4-templating.js'; +import 'prismjs/components/prism-t4-vb.js'; +import 'prismjs/components/prism-tap.js'; +import 'prismjs/components/prism-tcl.js'; +import 'prismjs/components/prism-textile.js'; +import 'prismjs/components/prism-toml.js'; +import 'prismjs/components/prism-tremor.js'; +import 'prismjs/components/prism-tsx.js'; +import 'prismjs/components/prism-tt2.js'; +import 'prismjs/components/prism-turtle.js'; +import 'prismjs/components/prism-twig.js'; +import 'prismjs/components/prism-typescript.js'; +import 'prismjs/components/prism-typoscript.js'; +import 'prismjs/components/prism-unrealscript.js'; +import 'prismjs/components/prism-uorazor.js'; +import 'prismjs/components/prism-uri.js'; +import 'prismjs/components/prism-v.js'; +import 'prismjs/components/prism-vala.js'; +import 'prismjs/components/prism-vbnet.js'; +import 'prismjs/components/prism-velocity.js'; +import 'prismjs/components/prism-verilog.js'; +import 'prismjs/components/prism-vhdl.js'; +import 'prismjs/components/prism-vim.js'; +import 'prismjs/components/prism-visual-basic.js'; +import 'prismjs/components/prism-warpscript.js'; +import 'prismjs/components/prism-wasm.js'; +import 'prismjs/components/prism-web-idl.js'; +import 'prismjs/components/prism-wgsl.js'; +import 'prismjs/components/prism-wiki.js'; +import 'prismjs/components/prism-wolfram.js'; +import 'prismjs/components/prism-wren.js'; +import 'prismjs/components/prism-xeora.js'; +import 'prismjs/components/prism-xml-doc.js'; +import 'prismjs/components/prism-xojo.js'; +import 'prismjs/components/prism-xquery.js'; +import 'prismjs/components/prism-yaml.js'; +import 'prismjs/components/prism-yang.js'; +import 'prismjs/components/prism-zig.js'; +import 'prismjs/components/prism-arduino.js'; + +// Broken: +// +// import 'prismjs/components/prism-bison.js'; +// import 'prismjs/components/prism-chaiscript.js'; +// import 'prismjs/components/prism-core.js'; +// import 'prismjs/components/prism-crystal.js'; +// import 'prismjs/components/prism-django.js'; +// import 'prismjs/components/prism-javadoc.js'; +// import 'prismjs/components/prism-jsdoc.js'; +// import 'prismjs/components/prism-plsql.js'; +// import 'prismjs/components/prism-racket.js'; +// import 'prismjs/components/prism-sparql.js'; +// import 'prismjs/components/prism-t4-cs.js'; import './ReactPrism.css'; // using classNames .prism-dark .prism-light from ReactPrism.css From 87fc490c3bf15fe98cf946325dfa835541c93da7 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sat, 5 Jul 2025 17:01:15 +0530 Subject: [PATCH 07/63] Fix new direct message showing with room (#2386) as we were mutating the content of m.direct the sdk was comparing old value with new one and preventing update if found equal --- src/client/action/room.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/action/room.js b/src/client/action/room.js index 90b74810..767914b5 100644 --- a/src/client/action/room.js +++ b/src/client/action/room.js @@ -12,7 +12,7 @@ function addRoomToMDirect(mx, roomId, userId) { const mDirectsEvent = mx.getAccountData('m.direct'); let userIdToRoomIds = {}; - if (typeof mDirectsEvent !== 'undefined') userIdToRoomIds = mDirectsEvent.getContent(); + if (typeof mDirectsEvent !== 'undefined') userIdToRoomIds = structuredClone(mDirectsEvent.getContent()); // remove it from the lists of any others users // (it can only be a DM room for one person) From 54ba1096d7e1a1ce4e8f34e0065726b6164cff60 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 5 Jul 2025 21:38:01 +1000 Subject: [PATCH 08/63] Bump nginx from 1.27.4-alpine to 1.29.0-alpine (#2382) Bumps nginx from 1.27.4-alpine to 1.29.0-alpine. --- updated-dependencies: - dependency-name: nginx dependency-version: 1.29.0-alpine dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index abb65ee5..718fed72 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,7 @@ RUN npm run build ## App -FROM nginx:1.27.4-alpine +FROM nginx:1.29.0-alpine COPY --from=builder /src/dist /app COPY --from=builder /src/docker-nginx.conf /etc/nginx/conf.d/default.conf From 3fd8a18157f61c9560323c7f4c54fc690b98332e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 5 Jul 2025 21:49:14 +1000 Subject: [PATCH 09/63] Bump dawidd6/action-download-artifact from 9 to 11 (#2364) Bumps [dawidd6/action-download-artifact](https://github.com/dawidd6/action-download-artifact) from 9 to 11. - [Release notes](https://github.com/dawidd6/action-download-artifact/releases) - [Commits](https://github.com/dawidd6/action-download-artifact/compare/07ab29fd4a977ae4d2b275087cf67563dfdf0295...ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5) --- updated-dependencies: - dependency-name: dawidd6/action-download-artifact dependency-version: '11' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/deploy-pull-request.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-pull-request.yml b/.github/workflows/deploy-pull-request.yml index 9c0bea78..b330c3c1 100644 --- a/.github/workflows/deploy-pull-request.yml +++ b/.github/workflows/deploy-pull-request.yml @@ -15,7 +15,7 @@ jobs: if: ${{ github.event.workflow_run.conclusion == 'success' }} steps: - name: Download pr number - uses: dawidd6/action-download-artifact@07ab29fd4a977ae4d2b275087cf67563dfdf0295 + uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 with: workflow: ${{ github.event.workflow.id }} run_id: ${{ github.event.workflow_run.id }} @@ -24,7 +24,7 @@ jobs: id: pr run: echo "id=$(> $GITHUB_OUTPUT - name: Download artifact - uses: dawidd6/action-download-artifact@07ab29fd4a977ae4d2b275087cf67563dfdf0295 + uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 with: workflow: ${{ github.event.workflow.id }} run_id: ${{ github.event.workflow_run.id }} From d0a7ef31bc3041f981994d78272509dc41da7cf6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 5 Jul 2025 21:51:29 +1000 Subject: [PATCH 10/63] Bump softprops/action-gh-release from 2.2.1 to 2.3.2 (#2363) Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.2.1 to 2.3.2. - [Release notes](https://github.com/softprops/action-gh-release/releases) - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) - [Commits](https://github.com/softprops/action-gh-release/compare/c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda...72f2c25fcb47643c292f7107632f7a47c1df5cd8) --- updated-dependencies: - dependency-name: softprops/action-gh-release dependency-version: 2.3.2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/prod-deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prod-deploy.yml b/.github/workflows/prod-deploy.yml index 0a758c51..d4a814b0 100644 --- a/.github/workflows/prod-deploy.yml +++ b/.github/workflows/prod-deploy.yml @@ -52,7 +52,7 @@ jobs: gpg --export | xxd -p echo '${{ secrets.GNUPG_PASSPHRASE }}' | gpg --batch --yes --pinentry-mode loopback --passphrase-fd 0 --armor --detach-sign cinny-${{ steps.vars.outputs.tag }}.tar.gz - name: Upload tagged release - uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda + uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 with: files: | cinny-${{ steps.vars.outputs.tag }}.tar.gz From c757b8967fcfc4c06b070e1f52266fa95dc1c4ac Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 5 Jul 2025 21:52:35 +1000 Subject: [PATCH 11/63] Update dependency vite to v5.4.19 [SECURITY] (#2326) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7dd2bf0a..3fc29010 100644 --- a/package-lock.json +++ b/package-lock.json @@ -99,7 +99,7 @@ "prettier": "2.8.1", "sass": "1.56.2", "typescript": "4.9.4", - "vite": "5.4.15", + "vite": "5.4.19", "vite-plugin-pwa": "0.20.5", "vite-plugin-static-copy": "1.0.4", "vite-plugin-top-level-await": "1.4.4" @@ -11331,9 +11331,9 @@ } }, "node_modules/vite": { - "version": "5.4.15", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.15.tgz", - "integrity": "sha512-6ANcZRivqL/4WtwPGTKNaosuNJr5tWiftOC7liM7G9+rMb8+oeJeyzymDu4rTN93seySBmbjSfsS3Vzr19KNtA==", + "version": "5.4.19", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", + "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", "license": "MIT", "dependencies": { "esbuild": "^0.21.3", diff --git a/package.json b/package.json index 3c1cef8c..81d0e20a 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,7 @@ "prettier": "2.8.1", "sass": "1.56.2", "typescript": "4.9.4", - "vite": "5.4.15", + "vite": "5.4.19", "vite-plugin-pwa": "0.20.5", "vite-plugin-static-copy": "1.0.4", "vite-plugin-top-level-await": "1.4.4" From 6b81401e2da8c1d1e61d297d256dc06d8f4e599b Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Fri, 11 Jul 2025 16:30:30 +0530 Subject: [PATCH 12/63] fix room not opening when two rooms has same alias (#2387) --- src/app/utils/matrix.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app/utils/matrix.ts b/src/app/utils/matrix.ts index a495e8d5..4b695724 100644 --- a/src/app/utils/matrix.ts +++ b/src/app/utils/matrix.ts @@ -50,7 +50,11 @@ export const getCanonicalAliasOrRoomId = (mx: MatrixClient, roomId: string): str const room = mx.getRoom(roomId); if (!room) return roomId; if (getStateEvent(room, StateEvent.RoomTombstone) !== undefined) return roomId; - return room.getCanonicalAlias() || roomId; + const alias = room.getCanonicalAlias(); + if (alias && getCanonicalAliasRoomId(mx, alias) === roomId) { + return alias; + } + return roomId; }; export const getImageInfo = (img: HTMLImageElement, fileOrBlob: File | Blob): IImageInfo => { From fbd7e0a14b4e50c7af69815d57254f97233296a3 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Fri, 11 Jul 2025 16:33:55 +0530 Subject: [PATCH 13/63] improve parent selection when opening a room (#2388) when a room has more than one orphan parent, we will select parent which has highest number of special users who have special powers in selected room. --- src/app/hooks/useRoomNavigate.ts | 24 +++++++++---------- src/app/utils/room.ts | 41 ++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 12 deletions(-) diff --git a/src/app/hooks/useRoomNavigate.ts b/src/app/hooks/useRoomNavigate.ts index e626c06b..b2d7a91a 100644 --- a/src/app/hooks/useRoomNavigate.ts +++ b/src/app/hooks/useRoomNavigate.ts @@ -9,7 +9,7 @@ import { getSpaceRoomPath, } from '../pages/pathUtils'; import { useMatrixClient } from './useMatrixClient'; -import { getOrphanParents } from '../utils/room'; +import { getOrphanParents, guessPerfectParent } from '../utils/room'; import { roomToParentsAtom } from '../state/room/roomToParents'; import { mDirectAtom } from '../state/mDirectList'; import { useSelectedSpace } from './router/useSelectedSpace'; @@ -39,19 +39,19 @@ export const useRoomNavigate = () => { const orphanParents = openSpaceTimeline ? [roomId] : getOrphanParents(roomToParents, roomId); if (orphanParents.length > 0) { - const pSpaceIdOrAlias = getCanonicalAliasOrRoomId( - mx, - spaceSelectedId && orphanParents.includes(spaceSelectedId) - ? spaceSelectedId - : orphanParents[0] // TODO: better orphan parent selection. - ); - - if (openSpaceTimeline) { - navigate(getSpaceRoomPath(pSpaceIdOrAlias, roomId, eventId), opts); - return; + let parentSpace: string; + if (spaceSelectedId && orphanParents.includes(spaceSelectedId)) { + parentSpace = spaceSelectedId; + } else { + parentSpace = guessPerfectParent(mx, roomId, orphanParents) ?? orphanParents[0]; } - navigate(getSpaceRoomPath(pSpaceIdOrAlias, roomIdOrAlias, eventId), opts); + const pSpaceIdOrAlias = getCanonicalAliasOrRoomId(mx, parentSpace); + + navigate( + getSpaceRoomPath(pSpaceIdOrAlias, openSpaceTimeline ? roomId : roomIdOrAlias, eventId), + opts + ); return; } diff --git a/src/app/utils/room.ts b/src/app/utils/room.ts index 79dcff9e..cae23514 100644 --- a/src/app/utils/room.ts +++ b/src/app/utils/room.ts @@ -5,6 +5,7 @@ import { EventTimelineSet, EventType, IMentions, + IPowerLevelsContent, IPushRule, IPushRules, JoinRule, @@ -473,3 +474,43 @@ export const bannedInRooms = (mx: MatrixClient, rooms: string[], otherUserId: st const banned = room.hasMembershipState(otherUserId, Membership.Ban); return banned; }); + +export const guessPerfectParent = ( + mx: MatrixClient, + roomId: string, + parents: string[] +): string | undefined => { + if (parents.length === 1) { + return parents[0]; + } + + const getSpecialUsers = (rId: string): string[] => { + const r = mx.getRoom(rId); + const powerLevels = + r && getStateEvent(r, StateEvent.RoomPowerLevels)?.getContent(); + + const { users_default: usersDefault, users } = powerLevels ?? {}; + if (typeof users !== 'object') return []; + + const defaultPower = typeof usersDefault === 'number' ? usersDefault : 0; + return Object.keys(users).filter((userId) => users[userId] > defaultPower); + }; + + let perfectParent: string | undefined; + let score = 0; + + const roomSpecialUsers = getSpecialUsers(roomId); + parents.forEach((parentId) => { + const parentSpecialUsers = getSpecialUsers(parentId); + const matchedUsersCount = parentSpecialUsers.filter((userId) => + roomSpecialUsers.includes(userId) + ).length; + + if (matchedUsersCount > score) { + score = matchedUsersCount; + perfectParent = parentId; + } + }); + + return perfectParent; +}; From c30c142653c1c0b4d9f514ff86b01b2db2d4e4b3 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Tue, 15 Jul 2025 18:03:45 +0530 Subject: [PATCH 14/63] Stop parsing servername from roomId (#2391) --- .../editor/autocomplete/RoomMentionAutocomplete.tsx | 4 ++-- .../editor/autocomplete/UserMentionAutocomplete.tsx | 4 ++-- src/app/molecules/space-add-existing/SpaceAddExisting.jsx | 6 +----- src/app/utils/matrix.ts | 6 +++--- src/client/action/room.js | 5 +---- 5 files changed, 9 insertions(+), 16 deletions(-) diff --git a/src/app/components/editor/autocomplete/RoomMentionAutocomplete.tsx b/src/app/components/editor/autocomplete/RoomMentionAutocomplete.tsx index cc431f58..b0c64f60 100644 --- a/src/app/components/editor/autocomplete/RoomMentionAutocomplete.tsx +++ b/src/app/components/editor/autocomplete/RoomMentionAutocomplete.tsx @@ -9,7 +9,7 @@ import { getDirectRoomAvatarUrl } from '../../../utils/room'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { AutocompleteQuery } from './autocompleteQuery'; import { AutocompleteMenu } from './AutocompleteMenu'; -import { getMxIdServer, validMxId } from '../../../utils/matrix'; +import { getMxIdServer, isRoomAlias } from '../../../utils/matrix'; import { UseAsyncSearchOptions, useAsyncSearch } from '../../../hooks/useAsyncSearch'; import { onTabPress } from '../../../utils/keyboard'; import { useKeyDown } from '../../../hooks/useKeyDown'; @@ -22,7 +22,7 @@ import { getViaServers } from '../../../plugins/via-servers'; type MentionAutoCompleteHandler = (roomAliasOrId: string, name: string) => void; const roomAliasFromQueryText = (mx: MatrixClient, text: string) => - validMxId(`#${text}`) + isRoomAlias(`#${text}`) ? `#${text}` : `#${text}${text.endsWith(':') ? '' : ':'}${getMxIdServer(mx.getUserId() ?? '')}`; diff --git a/src/app/components/editor/autocomplete/UserMentionAutocomplete.tsx b/src/app/components/editor/autocomplete/UserMentionAutocomplete.tsx index d6c0f302..7a8012eb 100644 --- a/src/app/components/editor/autocomplete/UserMentionAutocomplete.tsx +++ b/src/app/components/editor/autocomplete/UserMentionAutocomplete.tsx @@ -15,7 +15,7 @@ import { import { onTabPress } from '../../../utils/keyboard'; import { createMentionElement, moveCursor, replaceWithElement } from '../utils'; import { useKeyDown } from '../../../hooks/useKeyDown'; -import { getMxIdLocalPart, getMxIdServer, validMxId } from '../../../utils/matrix'; +import { getMxIdLocalPart, getMxIdServer, isUserId } from '../../../utils/matrix'; import { getMemberDisplayName, getMemberSearchStr } from '../../../utils/room'; import { UserAvatar } from '../../user-avatar'; import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; @@ -24,7 +24,7 @@ import { Membership } from '../../../../types/matrix/room'; type MentionAutoCompleteHandler = (userId: string, name: string) => void; const userIdFromQueryText = (mx: MatrixClient, text: string) => - validMxId(`@${text}`) + isUserId(`@${text}`) ? `@${text}` : `@${text}${text.endsWith(':') ? '' : ':'}${getMxIdServer(mx.getUserId() ?? '')}`; diff --git a/src/app/molecules/space-add-existing/SpaceAddExisting.jsx b/src/app/molecules/space-add-existing/SpaceAddExisting.jsx index b084a1ad..05b8d85f 100644 --- a/src/app/molecules/space-add-existing/SpaceAddExisting.jsx +++ b/src/app/molecules/space-add-existing/SpaceAddExisting.jsx @@ -5,7 +5,7 @@ import './SpaceAddExisting.scss'; import cons from '../../../client/state/cons'; import navigation from '../../../client/state/navigation'; -import { joinRuleToIconSrc, getIdServer } from '../../../util/matrixUtil'; +import { joinRuleToIconSrc } from '../../../util/matrixUtil'; import { Debounce } from '../../../util/common'; import Text from '../../atoms/text/Text'; @@ -21,7 +21,6 @@ import Dialog from '../dialog/Dialog'; import CrossIC from '../../../../public/res/ic/outlined/cross.svg'; import SearchIC from '../../../../public/res/ic/outlined/search.svg'; -import { useStore } from '../../hooks/useStore'; import { roomToParentsAtom } from '../../state/room/roomToParents'; import { useDirects, useRooms, useSpaces } from '../../state/hooks/roomList'; import { allRoomsAtom } from '../../state/room-list/roomList'; @@ -73,9 +72,6 @@ function SpaceAddExistingContent({ roomId, spaces: onlySpaces }) { await rateLimitedActions(selected, async (rId) => { const room = mx.getRoom(rId); const via = getViaServers(room); - if (via.length === 0) { - via.push(getIdServer(rId)); - } await mx.sendStateEvent( roomId, diff --git a/src/app/utils/matrix.ts b/src/app/utils/matrix.ts index 4b695724..610ef0af 100644 --- a/src/app/utils/matrix.ts +++ b/src/app/utils/matrix.ts @@ -23,9 +23,9 @@ const DOMAIN_REGEX = /\b(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}\b/; export const isServerName = (serverName: string): boolean => DOMAIN_REGEX.test(serverName); -export const matchMxId = (id: string): RegExpMatchArray | null => id.match(/^([@!$+#])(.+):(\S+)$/); +const matchMxId = (id: string): RegExpMatchArray | null => id.match(/^([@$+#])(.+):(\S+)$/); -export const validMxId = (id: string): boolean => !!matchMxId(id); +const validMxId = (id: string): boolean => !!matchMxId(id); export const getMxIdServer = (userId: string): string | undefined => matchMxId(userId)?.[3]; @@ -33,7 +33,7 @@ export const getMxIdLocalPart = (userId: string): string | undefined => matchMxI export const isUserId = (id: string): boolean => validMxId(id) && id.startsWith('@'); -export const isRoomId = (id: string): boolean => validMxId(id) && id.startsWith('!'); +export const isRoomId = (id: string): boolean => id.startsWith('!'); export const isRoomAlias = (id: string): boolean => validMxId(id) && id.startsWith('#'); diff --git a/src/client/action/room.js b/src/client/action/room.js index 767914b5..e39aeed8 100644 --- a/src/client/action/room.js +++ b/src/client/action/room.js @@ -93,11 +93,8 @@ function convertToRoom(mx, roomId) { * @param {string[]} via */ async function join(mx, roomIdOrAlias, isDM = false, via = undefined) { - const roomIdParts = roomIdOrAlias.split(':'); - const viaServers = via || [roomIdParts[1]]; - try { - const resultRoom = await mx.joinRoom(roomIdOrAlias, { viaServers }); + const resultRoom = await mx.joinRoom(roomIdOrAlias, { viaServers: via }); if (isDM) { const targetUserId = guessDMRoomTargetId(mx.getRoom(resultRoom.roomId), mx.getUserId()); From c462a3b8d5f9948eed3de76c1c3180d41a00fbdc Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Tue, 15 Jul 2025 18:10:16 +0530 Subject: [PATCH 15/63] Link device account management with OIDC (#2390) * load auth metadata configs on startup * deep-link cross-signing reset button with oidc * deep-link manage devices and delete device with oidc * fix import typo --- .../CapabilitiesAndMediaConfigLoader.tsx | 36 --------- src/app/components/ServerConfigsLoader.tsx | 52 ++++++++++++ .../settings/devices/OtherDevices.tsx | 80 +++++++++++++++++-- .../settings/devices/Verification.tsx | 17 ++++ src/app/hooks/useAccountManagement.ts | 17 ++++ src/app/hooks/useAuthMetadata.ts | 12 +++ src/app/pages/client/ClientRoot.tsx | 23 +++--- 7 files changed, 185 insertions(+), 52 deletions(-) delete mode 100644 src/app/components/CapabilitiesAndMediaConfigLoader.tsx create mode 100644 src/app/components/ServerConfigsLoader.tsx create mode 100644 src/app/hooks/useAccountManagement.ts create mode 100644 src/app/hooks/useAuthMetadata.ts diff --git a/src/app/components/CapabilitiesAndMediaConfigLoader.tsx b/src/app/components/CapabilitiesAndMediaConfigLoader.tsx deleted file mode 100644 index 574d0ca7..00000000 --- a/src/app/components/CapabilitiesAndMediaConfigLoader.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { ReactNode, useCallback, useEffect } from 'react'; -import { Capabilities } from 'matrix-js-sdk'; -import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback'; -import { useMatrixClient } from '../hooks/useMatrixClient'; -import { MediaConfig } from '../hooks/useMediaConfig'; -import { promiseFulfilledResult } from '../utils/common'; - -type CapabilitiesAndMediaConfigLoaderProps = { - children: (capabilities?: Capabilities, mediaConfig?: MediaConfig) => ReactNode; -}; -export function CapabilitiesAndMediaConfigLoader({ - children, -}: CapabilitiesAndMediaConfigLoaderProps) { - const mx = useMatrixClient(); - - const [state, load] = useAsyncCallback< - [Capabilities | undefined, MediaConfig | undefined], - unknown, - [] - >( - useCallback(async () => { - const result = await Promise.allSettled([mx.getCapabilities(), mx.getMediaConfig()]); - const capabilities = promiseFulfilledResult(result[0]); - const mediaConfig = promiseFulfilledResult(result[1]); - return [capabilities, mediaConfig]; - }, [mx]) - ); - - useEffect(() => { - load(); - }, [load]); - - const [capabilities, mediaConfig] = - state.status === AsyncStatus.Success ? state.data : [undefined, undefined]; - return children(capabilities, mediaConfig); -} diff --git a/src/app/components/ServerConfigsLoader.tsx b/src/app/components/ServerConfigsLoader.tsx new file mode 100644 index 00000000..3c8ce8eb --- /dev/null +++ b/src/app/components/ServerConfigsLoader.tsx @@ -0,0 +1,52 @@ +import { ReactNode, useCallback, useMemo } from 'react'; +import { Capabilities, validateAuthMetadata, ValidatedAuthMetadata } from 'matrix-js-sdk'; +import { AsyncStatus, useAsyncCallbackValue } from '../hooks/useAsyncCallback'; +import { useMatrixClient } from '../hooks/useMatrixClient'; +import { MediaConfig } from '../hooks/useMediaConfig'; +import { promiseFulfilledResult } from '../utils/common'; + +export type ServerConfigs = { + capabilities?: Capabilities; + mediaConfig?: MediaConfig; + authMetadata?: ValidatedAuthMetadata; +}; + +type ServerConfigsLoaderProps = { + children: (configs: ServerConfigs) => ReactNode; +}; +export function ServerConfigsLoader({ children }: ServerConfigsLoaderProps) { + const mx = useMatrixClient(); + const fallbackConfigs = useMemo(() => ({}), []); + + const [configsState] = useAsyncCallbackValue( + useCallback(async () => { + const result = await Promise.allSettled([ + mx.getCapabilities(), + mx.getMediaConfig(), + mx.getAuthMetadata(), + ]); + + const capabilities = promiseFulfilledResult(result[0]); + const mediaConfig = promiseFulfilledResult(result[1]); + const authMetadata = promiseFulfilledResult(result[2]); + let validatedAuthMetadata: ValidatedAuthMetadata | undefined; + + try { + validatedAuthMetadata = validateAuthMetadata(authMetadata); + } catch (e) { + console.error(e); + } + + return { + capabilities, + mediaConfig, + authMetadata: validatedAuthMetadata, + }; + }, [mx]) + ); + + const configs: ServerConfigs = + configsState.status === AsyncStatus.Success ? configsState.data : fallbackConfigs; + + return children(configs); +} diff --git a/src/app/features/settings/devices/OtherDevices.tsx b/src/app/features/settings/devices/OtherDevices.tsx index 0d879e59..4bd83dd6 100644 --- a/src/app/features/settings/devices/OtherDevices.tsx +++ b/src/app/features/settings/devices/OtherDevices.tsx @@ -11,6 +11,10 @@ import { useUIAMatrixError } from '../../../hooks/useUIAFlows'; import { DeviceVerificationStatus } from '../../../components/DeviceVerificationStatus'; import { VerifyOtherDeviceTile } from './Verification'; import { VerificationStatus } from '../../../hooks/useDeviceVerificationStatus'; +import { useAuthMetadata } from '../../../hooks/useAuthMetadata'; +import { withSearchParam } from '../../../pages/pathUtils'; +import { useAccountManagementActions } from '../../../hooks/useAccountManagement'; +import { SettingTile } from '../../../components/setting-tile'; type OtherDevicesProps = { devices: IMyDevice[]; @@ -20,8 +24,39 @@ type OtherDevicesProps = { export function OtherDevices({ devices, refreshDeviceList, showVerification }: OtherDevicesProps) { const mx = useMatrixClient(); const crypto = mx.getCrypto(); + const authMetadata = useAuthMetadata(); + const accountManagementActions = useAccountManagementActions(); + const [deleted, setDeleted] = useState>(new Set()); + const handleDashboardOIDC = useCallback(() => { + const authUrl = authMetadata?.account_management_uri ?? authMetadata?.issuer; + if (!authUrl) return; + + window.open( + withSearchParam(authUrl, { + action: accountManagementActions.sessionsList, + }), + '_blank' + ); + }, [authMetadata, accountManagementActions]); + + const handleDeleteOIDC = useCallback( + (deviceId: string) => { + const authUrl = authMetadata?.account_management_uri ?? authMetadata?.issuer; + if (!authUrl) return; + + window.open( + withSearchParam(authUrl, { + action: accountManagementActions.sessionEnd, + device_id: deviceId, + }), + '_blank' + ); + }, + [authMetadata, accountManagementActions] + ); + const handleToggleDelete = useCallback((deviceId: string) => { setDeleted((deviceIds) => { const newIds = new Set(deviceIds); @@ -70,6 +105,31 @@ export function OtherDevices({ devices, refreshDeviceList, showVerification }: O <> Others + {authMetadata && ( + + + Open + + } + /> + + )} {devices .sort((d1, d2) => { if (!d1.last_seen_ts || !d2.last_seen_ts) return 0; @@ -89,12 +149,20 @@ export function OtherDevices({ devices, refreshDeviceList, showVerification }: O refreshDeviceList={refreshDeviceList} disabled={deleting} options={ - + authMetadata ? ( + + ) : ( + + ) } /> {showVerification && crypto && ( diff --git a/src/app/features/settings/devices/Verification.tsx b/src/app/features/settings/devices/Verification.tsx index 59fa6b67..6c7eab17 100644 --- a/src/app/features/settings/devices/Verification.tsx +++ b/src/app/features/settings/devices/Verification.tsx @@ -32,6 +32,9 @@ import { DeviceVerificationSetup, } from '../../../components/DeviceVerificationSetup'; import { stopPropagation } from '../../../utils/keyboard'; +import { useAuthMetadata } from '../../../hooks/useAuthMetadata'; +import { withSearchParam } from '../../../pages/pathUtils'; +import { useAccountManagementActions } from '../../../hooks/useAccountManagement'; type VerificationStatusBadgeProps = { verificationStatus: VerificationStatus; @@ -252,6 +255,8 @@ export function EnableVerification({ visible }: EnableVerificationProps) { export function DeviceVerificationOptions() { const [menuCords, setMenuCords] = useState(); + const authMetadata = useAuthMetadata(); + const accountManagementActions = useAccountManagementActions(); const [reset, setReset] = useState(false); @@ -265,6 +270,18 @@ export function DeviceVerificationOptions() { const handleReset = () => { setMenuCords(undefined); + + if (authMetadata) { + const authUrl = authMetadata.account_management_uri ?? authMetadata.issuer; + window.open( + withSearchParam(authUrl, { + action: accountManagementActions.crossSigningReset, + }), + '_blank' + ); + return; + } + setReset(true); }; diff --git a/src/app/hooks/useAccountManagement.ts b/src/app/hooks/useAccountManagement.ts new file mode 100644 index 00000000..5eafedc4 --- /dev/null +++ b/src/app/hooks/useAccountManagement.ts @@ -0,0 +1,17 @@ +import { useMemo } from 'react'; + +export const useAccountManagementActions = () => { + const actions = useMemo( + () => ({ + profile: 'org.matrix.profile', + sessionsList: 'org.matrix.sessions_list', + sessionView: 'org.matrix.session_view', + sessionEnd: 'org.matrix.session_end', + accountDeactivate: 'org.matrix.account_deactivate', + crossSigningReset: 'org.matrix.cross_signing_reset', + }), + [] + ); + + return actions; +}; diff --git a/src/app/hooks/useAuthMetadata.ts b/src/app/hooks/useAuthMetadata.ts new file mode 100644 index 00000000..db967463 --- /dev/null +++ b/src/app/hooks/useAuthMetadata.ts @@ -0,0 +1,12 @@ +import { ValidatedAuthMetadata } from 'matrix-js-sdk'; +import { createContext, useContext } from 'react'; + +const AuthMetadataContext = createContext(undefined); + +export const AuthMetadataProvider = AuthMetadataContext.Provider; + +export const useAuthMetadata = (): ValidatedAuthMetadata | undefined => { + const metadata = useContext(AuthMetadataContext); + + return metadata; +}; diff --git a/src/app/pages/client/ClientRoot.tsx b/src/app/pages/client/ClientRoot.tsx index 846d8ff3..c48dbf53 100644 --- a/src/app/pages/client/ClientRoot.tsx +++ b/src/app/pages/client/ClientRoot.tsx @@ -25,7 +25,7 @@ import { } from '../../../client/initMatrix'; import { getSecret } from '../../../client/state/auth'; import { SplashScreen } from '../../components/splash-screen'; -import { CapabilitiesAndMediaConfigLoader } from '../../components/CapabilitiesAndMediaConfigLoader'; +import { ServerConfigsLoader } from '../../components/ServerConfigsLoader'; import { CapabilitiesProvider } from '../../hooks/useCapabilities'; import { MediaConfigProvider } from '../../hooks/useMediaConfig'; import { MatrixClientProvider } from '../../hooks/useMatrixClient'; @@ -37,6 +37,7 @@ import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; import { useSyncState } from '../../hooks/useSyncState'; import { stopPropagation } from '../../utils/keyboard'; import { SyncStatus } from './SyncStatus'; +import { AuthMetadataProvider } from '../../hooks/useAuthMetadata'; function ClientRootLoading() { return ( @@ -207,18 +208,20 @@ export function ClientRoot({ children }: ClientRootProps) { ) : ( - - {(capabilities, mediaConfig) => ( - - - {children} - - - + + {(serverConfigs) => ( + + + + {children} + + + + )} - + )} From 50cc78788f8b1da50898b9863fdba9b714550e52 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Tue, 15 Jul 2025 18:11:33 +0530 Subject: [PATCH 16/63] Jump to time option in room timeline (#2377) * add time and date picker components * add time utils * add jump to time in room timeline * fix typo causing crash in safari --- src/app/components/time-date/DatePicker.tsx | 129 +++++++++ src/app/components/time-date/PickerColumn.tsx | 23 ++ src/app/components/time-date/TimePicker.tsx | 132 +++++++++ src/app/components/time-date/index.ts | 2 + src/app/components/time-date/styles.css.ts | 16 ++ src/app/features/room/RoomViewHeader.tsx | 30 ++ .../features/room/jump-to-time/JumpToTime.tsx | 256 ++++++++++++++++++ src/app/features/room/jump-to-time/index.ts | 1 + src/app/utils/time.ts | 48 ++++ 9 files changed, 637 insertions(+) create mode 100644 src/app/components/time-date/DatePicker.tsx create mode 100644 src/app/components/time-date/PickerColumn.tsx create mode 100644 src/app/components/time-date/TimePicker.tsx create mode 100644 src/app/components/time-date/index.ts create mode 100644 src/app/components/time-date/styles.css.ts create mode 100644 src/app/features/room/jump-to-time/JumpToTime.tsx create mode 100644 src/app/features/room/jump-to-time/index.ts diff --git a/src/app/components/time-date/DatePicker.tsx b/src/app/components/time-date/DatePicker.tsx new file mode 100644 index 00000000..faa43a3f --- /dev/null +++ b/src/app/components/time-date/DatePicker.tsx @@ -0,0 +1,129 @@ +import React, { forwardRef } from 'react'; +import { Menu, Box, Text, Chip } from 'folds'; +import dayjs from 'dayjs'; +import * as css from './styles.css'; +import { PickerColumn } from './PickerColumn'; +import { dateFor, daysInMonth, daysToMs } from '../../utils/time'; + +type DatePickerProps = { + min: number; + max: number; + value: number; + onChange: (value: number) => void; +}; +export const DatePicker = forwardRef( + ({ min, max, value, onChange }, ref) => { + const selectedYear = dayjs(value).year(); + const selectedMonth = dayjs(value).month() + 1; + const selectedDay = dayjs(value).date(); + + const handleSubmit = (newValue: number) => { + onChange(Math.min(Math.max(min, newValue), max)); + }; + + const handleDay = (day: number) => { + const seconds = daysToMs(day); + const lastSeconds = daysToMs(selectedDay); + const newValue = value + (seconds - lastSeconds); + handleSubmit(newValue); + }; + + const handleMonthAndYear = (month: number, year: number) => { + const mDays = daysInMonth(month, year); + const currentDate = dateFor(selectedYear, selectedMonth, selectedDay); + const time = value - currentDate; + + const newDate = dateFor(year, month, mDays < selectedDay ? mDays : selectedDay); + + const newValue = newDate + time; + handleSubmit(newValue); + }; + + const handleMonth = (month: number) => { + handleMonthAndYear(month, selectedYear); + }; + + const handleYear = (year: number) => { + handleMonthAndYear(selectedMonth, year); + }; + + const minYear = dayjs(min).year(); + const maxYear = dayjs(max).year(); + const yearsRange = maxYear - minYear + 1; + + const minMonth = dayjs(min).month() + 1; + const maxMonth = dayjs(max).month() + 1; + + const minDay = dayjs(min).date(); + const maxDay = dayjs(max).date(); + return ( + + + + {Array.from(Array(daysInMonth(selectedMonth, selectedYear)).keys()) + .map((i) => i + 1) + .map((day) => ( + handleDay(day)} + disabled={ + (selectedYear === minYear && selectedMonth === minMonth && day < minDay) || + (selectedYear === maxYear && selectedMonth === maxMonth && day > maxDay) + } + > + {day} + + ))} + + + {Array.from(Array(12).keys()) + .map((i) => i + 1) + .map((month) => ( + handleMonth(month)} + disabled={ + (selectedYear === minYear && month < minMonth) || + (selectedYear === maxYear && month > maxMonth) + } + > + + {dayjs() + .month(month - 1) + .format('MMM')} + + + ))} + + + {Array.from(Array(yearsRange).keys()) + .map((i) => minYear + i) + .map((year) => ( + handleYear(year)} + > + {year} + + ))} + + + + ); + } +); diff --git a/src/app/components/time-date/PickerColumn.tsx b/src/app/components/time-date/PickerColumn.tsx new file mode 100644 index 00000000..c31daf43 --- /dev/null +++ b/src/app/components/time-date/PickerColumn.tsx @@ -0,0 +1,23 @@ +import React, { ReactNode } from 'react'; +import { Box, Text, Scroll } from 'folds'; +import { CutoutCard } from '../cutout-card'; +import * as css from './styles.css'; + +export function PickerColumn({ title, children }: { title: string; children: ReactNode }) { + return ( + + + {title} + + + + + + {children} + + + + + + ); +} diff --git a/src/app/components/time-date/TimePicker.tsx b/src/app/components/time-date/TimePicker.tsx new file mode 100644 index 00000000..1dd0958b --- /dev/null +++ b/src/app/components/time-date/TimePicker.tsx @@ -0,0 +1,132 @@ +import React, { forwardRef } from 'react'; +import { Menu, Box, Text, Chip } from 'folds'; +import dayjs from 'dayjs'; +import * as css from './styles.css'; +import { PickerColumn } from './PickerColumn'; +import { hour12to24, hour24to12, hoursToMs, inSameDay, minutesToMs } from '../../utils/time'; + +type TimePickerProps = { + min: number; + max: number; + value: number; + onChange: (value: number) => void; +}; +export const TimePicker = forwardRef( + ({ min, max, value, onChange }, ref) => { + const hour24 = dayjs(value).hour(); + + const selectedHour = hour24to12(hour24); + const selectedMinute = dayjs(value).minute(); + const selectedPM = hour24 >= 12; + + const handleSubmit = (newValue: number) => { + onChange(Math.min(Math.max(min, newValue), max)); + }; + + const handleHour = (hour: number) => { + const seconds = hoursToMs(hour12to24(hour, selectedPM)); + const lastSeconds = hoursToMs(hour24); + const newValue = value + (seconds - lastSeconds); + handleSubmit(newValue); + }; + + const handleMinute = (minute: number) => { + const seconds = minutesToMs(minute); + const lastSeconds = minutesToMs(selectedMinute); + const newValue = value + (seconds - lastSeconds); + handleSubmit(newValue); + }; + + const handlePeriod = (pm: boolean) => { + const seconds = hoursToMs(hour12to24(selectedHour, pm)); + const lastSeconds = hoursToMs(hour24); + const newValue = value + (seconds - lastSeconds); + handleSubmit(newValue); + }; + + const minHour24 = dayjs(min).hour(); + const maxHour24 = dayjs(max).hour(); + + const minMinute = dayjs(min).minute(); + const maxMinute = dayjs(max).minute(); + const minPM = minHour24 >= 12; + const maxPM = maxHour24 >= 12; + + const minDay = inSameDay(min, value); + const maxDay = inSameDay(max, value); + + return ( + + + + {Array.from(Array(12).keys()) + .map((i) => { + if (i === 0) return 12; + return i; + }) + .map((hour) => ( + handleHour(hour)} + disabled={ + (minDay && hour12to24(hour, selectedPM) < minHour24) || + (maxDay && hour12to24(hour, selectedPM) > maxHour24) + } + > + {hour < 10 ? `0${hour}` : hour} + + ))} + + + {Array.from(Array(60).keys()).map((minute) => ( + handleMinute(minute)} + disabled={ + (minDay && hour24 === minHour24 && minute < minMinute) || + (maxDay && hour24 === maxHour24 && minute > maxMinute) + } + > + {minute < 10 ? `0${minute}` : minute} + + ))} + + + handlePeriod(false)} + disabled={minDay && minPM} + > + AM + + handlePeriod(true)} + disabled={maxDay && !maxPM} + > + PM + + + + + ); + } +); diff --git a/src/app/components/time-date/index.ts b/src/app/components/time-date/index.ts new file mode 100644 index 00000000..592c5af7 --- /dev/null +++ b/src/app/components/time-date/index.ts @@ -0,0 +1,2 @@ +export * from './TimePicker'; +export * from './DatePicker'; diff --git a/src/app/components/time-date/styles.css.ts b/src/app/components/time-date/styles.css.ts new file mode 100644 index 00000000..97926d3f --- /dev/null +++ b/src/app/components/time-date/styles.css.ts @@ -0,0 +1,16 @@ +import { style } from '@vanilla-extract/css'; +import { config, toRem } from 'folds'; + +export const PickerMenu = style({ + padding: config.space.S200, +}); +export const PickerContainer = style({ + maxHeight: toRem(250), +}); +export const PickerColumnLabel = style({ + padding: config.space.S200, +}); +export const PickerColumnContent = style({ + padding: config.space.S200, + paddingRight: 0, +}); diff --git a/src/app/features/room/RoomViewHeader.tsx b/src/app/features/room/RoomViewHeader.tsx index 352ae4b5..63e9d55d 100644 --- a/src/app/features/room/RoomViewHeader.tsx +++ b/src/app/features/room/RoomViewHeader.tsx @@ -65,6 +65,8 @@ import { getRoomNotificationModeIcon, useRoomsNotificationPreferencesContext, } from '../../hooks/useRoomsNotificationPreferences'; +import { JumpToTime } from './jump-to-time'; +import { useRoomNavigate } from '../../hooks/useRoomNavigate'; type RoomMenuProps = { room: Room; @@ -79,6 +81,7 @@ const RoomMenu = forwardRef(({ room, requestClose const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? '')); const notificationPreferences = useRoomsNotificationPreferencesContext(); const notificationMode = getRoomNotificationMode(notificationPreferences, room.roomId); + const { navigateRoom } = useRoomNavigate(); const handleMarkAsRead = () => { markAsRead(mx, room.roomId, hideActivity); @@ -175,6 +178,33 @@ const RoomMenu = forwardRef(({ room, requestClose Room Settings + + {(promptJump, setPromptJump) => ( + <> + setPromptJump(true)} + size="300" + after={} + radii="300" + aria-pressed={promptJump} + > + + Jump to Time + + + {promptJump && ( + { + setPromptJump(false); + navigateRoom(room.roomId, eventId); + requestClose(); + }} + onCancel={() => setPromptJump(false)} + /> + )} + + )} + diff --git a/src/app/features/room/jump-to-time/JumpToTime.tsx b/src/app/features/room/jump-to-time/JumpToTime.tsx new file mode 100644 index 00000000..8c4e2c0b --- /dev/null +++ b/src/app/features/room/jump-to-time/JumpToTime.tsx @@ -0,0 +1,256 @@ +import React, { MouseEventHandler, useCallback, useMemo, useState } from 'react'; +import FocusTrap from 'focus-trap-react'; +import { + Dialog, + Overlay, + OverlayCenter, + OverlayBackdrop, + Header, + config, + Box, + Text, + IconButton, + Icon, + Icons, + color, + Button, + Spinner, + Chip, + PopOut, + RectCords, +} from 'folds'; +import { Direction, MatrixError } from 'matrix-js-sdk'; +import { useMatrixClient } from '../../../hooks/useMatrixClient'; +import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; +import { stopPropagation } from '../../../utils/keyboard'; +import { useAlive } from '../../../hooks/useAlive'; +import { useStateEvent } from '../../../hooks/useStateEvent'; +import { useRoom } from '../../../hooks/useRoom'; +import { StateEvent } from '../../../../types/matrix/room'; +import { getToday, getYesterday, timeDayMonthYear, timeHourMinute } from '../../../utils/time'; +import { DatePicker, TimePicker } from '../../../components/time-date'; + +type JumpToTimeProps = { + onCancel: () => void; + onSubmit: (eventId: string) => void; +}; +export function JumpToTime({ onCancel, onSubmit }: JumpToTimeProps) { + const mx = useMatrixClient(); + const room = useRoom(); + const alive = useAlive(); + const createStateEvent = useStateEvent(room, StateEvent.RoomCreate); + + const todayTs = getToday(); + const yesterdayTs = getYesterday(); + const createTs = useMemo(() => createStateEvent?.getTs() ?? 0, [createStateEvent]); + const [ts, setTs] = useState(() => Date.now()); + + const [timePickerCords, setTimePickerCords] = useState(); + const [datePickerCords, setDatePickerCords] = useState(); + + const handleTimePicker: MouseEventHandler = (evt) => { + setTimePickerCords(evt.currentTarget.getBoundingClientRect()); + }; + const handleDatePicker: MouseEventHandler = (evt) => { + setDatePickerCords(evt.currentTarget.getBoundingClientRect()); + }; + + const handleToday = () => { + setTs(todayTs < createTs ? createTs : todayTs); + }; + const handleYesterday = () => { + setTs(yesterdayTs < createTs ? createTs : yesterdayTs); + }; + const handleBeginning = () => setTs(createTs); + + const [timestampState, timestampToEvent] = useAsyncCallback( + useCallback( + async (newTs) => { + const result = await mx.timestampToEvent(room.roomId, newTs, Direction.Forward); + return result.event_id; + }, + [mx, room] + ) + ); + + const handleSubmit = () => { + timestampToEvent(ts).then((eventId) => { + if (alive()) { + onSubmit(eventId); + } + }); + }; + + return ( + }> + + + +
+ + Jump to Time + + + + +
+ + + + + Time + + + } + onClick={handleTimePicker} + > + {timeHourMinute(ts)} + + setTimePickerCords(undefined), + clickOutsideDeactivates: true, + isKeyForward: (evt: KeyboardEvent) => + evt.key === 'ArrowDown' || evt.key === 'ArrowRight', + isKeyBackward: (evt: KeyboardEvent) => + evt.key === 'ArrowUp' || evt.key === 'ArrowLeft', + escapeDeactivates: stopPropagation, + }} + > + +
+ } + /> +
+ + + + Date + + + } + onClick={handleDatePicker} + > + {timeDayMonthYear(ts)} + + setDatePickerCords(undefined), + clickOutsideDeactivates: true, + isKeyForward: (evt: KeyboardEvent) => + evt.key === 'ArrowDown' || evt.key === 'ArrowRight', + isKeyBackward: (evt: KeyboardEvent) => + evt.key === 'ArrowUp' || evt.key === 'ArrowLeft', + escapeDeactivates: stopPropagation, + }} + > + + + } + /> + + + + + Preset + + {createTs < todayTs && ( + + Today + + )} + {createTs < yesterdayTs && ( + + Yesterday + + )} + + Beginning + + + + {timestampState.status === AsyncStatus.Error && ( + + {timestampState.error.message} + + )} + + + + + + + ); +} diff --git a/src/app/features/room/jump-to-time/index.ts b/src/app/features/room/jump-to-time/index.ts new file mode 100644 index 00000000..9bdc2c74 --- /dev/null +++ b/src/app/features/room/jump-to-time/index.ts @@ -0,0 +1 @@ +export * from './JumpToTime'; diff --git a/src/app/utils/time.ts b/src/app/utils/time.ts index 3ee6720c..f230e59b 100644 --- a/src/app/utils/time.ts +++ b/src/app/utils/time.ts @@ -9,12 +9,26 @@ export const today = (ts: number): boolean => dayjs(ts).isToday(); export const yesterday = (ts: number): boolean => dayjs(ts).isYesterday(); +export const timeHour = (ts: number): string => dayjs(ts).format('hh'); +export const timeMinute = (ts: number): string => dayjs(ts).format('mm'); +export const timeAmPm = (ts: number): string => dayjs(ts).format('A'); +export const timeDay = (ts: number): string => dayjs(ts).format('D'); +export const timeMon = (ts: number): string => dayjs(ts).format('MMM'); +export const timeMonth = (ts: number): string => dayjs(ts).format('MMMM'); +export const timeYear = (ts: number): string => dayjs(ts).format('YYYY'); + export const timeHourMinute = (ts: number): string => dayjs(ts).format('hh:mm A'); export const timeDayMonYear = (ts: number): string => dayjs(ts).format('D MMM YYYY'); export const timeDayMonthYear = (ts: number): string => dayjs(ts).format('D MMMM YYYY'); +export const daysInMonth = (month: number, year: number): number => + dayjs(`${year}-${month}-1`).daysInMonth(); + +export const dateFor = (year: number, month: number, day: number): number => + dayjs(`${year}-${month}-${day}`).valueOf(); + export const inSameDay = (ts1: number, ts2: number): boolean => { const dt1 = new Date(ts1); const dt2 = new Date(ts2); @@ -33,3 +47,37 @@ export const minuteDifference = (ts1: number, ts2: number): number => { diff /= 60; return Math.abs(Math.round(diff)); }; + +export const hour24to12 = (hour24: number): number => { + const h = hour24 % 12; + + if (h === 0) return 12; + return h; +}; + +export const hour12to24 = (hour: number, pm: boolean): number => { + if (hour === 12) { + return pm ? 12 : 0; + } + return pm ? hour + 12 : hour; +}; + +export const secondsToMs = (seconds: number) => seconds * 1000; + +export const minutesToMs = (minutes: number) => minutes * secondsToMs(60); + +export const hoursToMs = (hour: number) => hour * minutesToMs(60); + +export const daysToMs = (days: number) => days * hoursToMs(24); + +export const getToday = () => { + const nowTs = Date.now(); + const date = dayjs(nowTs); + return dateFor(date.year(), date.month() + 1, date.date()); +}; + +export const getYesterday = () => { + const nowTs = Date.now() - daysToMs(1); + const date = dayjs(nowTs); + return dateFor(date.year(), date.month() + 1, date.date()); +}; From acc7d4ff565fa6f8f0666920c5959f43be28d9ea Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Wed, 16 Jul 2025 16:19:13 +0530 Subject: [PATCH 17/63] Support oidc action param for login and register page (#2389) --- src/app/pages/auth/SSOLogin.tsx | 8 +++++--- src/app/pages/auth/login/Login.tsx | 2 ++ src/app/pages/auth/register/Register.tsx | 2 ++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/app/pages/auth/SSOLogin.tsx b/src/app/pages/auth/SSOLogin.tsx index d0cdaeb6..3ff1a229 100644 --- a/src/app/pages/auth/SSOLogin.tsx +++ b/src/app/pages/auth/SSOLogin.tsx @@ -1,19 +1,21 @@ import { Avatar, AvatarImage, Box, Button, Text } from 'folds'; -import { IIdentityProvider, createClient } from 'matrix-js-sdk'; +import { IIdentityProvider, SSOAction, createClient } from 'matrix-js-sdk'; import React, { useMemo } from 'react'; import { useAutoDiscoveryInfo } from '../../hooks/useAutoDiscoveryInfo'; type SSOLoginProps = { providers?: IIdentityProvider[]; redirectUrl: string; + action?: SSOAction; saveScreenSpace?: boolean; }; -export function SSOLogin({ providers, redirectUrl, saveScreenSpace }: SSOLoginProps) { +export function SSOLogin({ providers, redirectUrl, action, saveScreenSpace }: SSOLoginProps) { const discovery = useAutoDiscoveryInfo(); const baseUrl = discovery['m.homeserver'].base_url; const mx = useMemo(() => createClient({ baseUrl }), [baseUrl]); - const getSSOIdUrl = (ssoId?: string): string => mx.getSsoLoginUrl(redirectUrl, 'sso', ssoId); + const getSSOIdUrl = (ssoId?: string): string => + mx.getSsoLoginUrl(redirectUrl, 'sso', ssoId, action); const withoutIcon = providers ? providers.find( diff --git a/src/app/pages/auth/login/Login.tsx b/src/app/pages/auth/login/Login.tsx index 6b9f1223..2f04a733 100644 --- a/src/app/pages/auth/login/Login.tsx +++ b/src/app/pages/auth/login/Login.tsx @@ -1,6 +1,7 @@ import React, { useMemo } from 'react'; import { Box, Text, color } from 'folds'; import { Link, useSearchParams } from 'react-router-dom'; +import { SSOAction } from 'matrix-js-sdk'; import { useAuthFlows } from '../../../hooks/useAuthFlows'; import { useAuthServer } from '../../../hooks/useAuthServer'; import { useParsedLoginFlows } from '../../../hooks/useParsedLoginFlows'; @@ -76,6 +77,7 @@ export function Login() { diff --git a/src/app/pages/auth/register/Register.tsx b/src/app/pages/auth/register/Register.tsx index d2986d70..7176489b 100644 --- a/src/app/pages/auth/register/Register.tsx +++ b/src/app/pages/auth/register/Register.tsx @@ -1,6 +1,7 @@ import React, { useMemo } from 'react'; import { Box, Text, color } from 'folds'; import { Link, useSearchParams } from 'react-router-dom'; +import { SSOAction } from 'matrix-js-sdk'; import { useAuthServer } from '../../../hooks/useAuthServer'; import { RegisterFlowStatus, useAuthFlows } from '../../../hooks/useAuthFlows'; import { useParsedLoginFlows } from '../../../hooks/useParsedLoginFlows'; @@ -83,6 +84,7 @@ export function Register() { From 3cdb5c2fe6483e08ea49ff4f90209fd22dc329fc Mon Sep 17 00:00:00 2001 From: Gimle Larpes <97182804+GimleLarpes@users.noreply.github.com> Date: Wed, 23 Jul 2025 18:10:56 +0300 Subject: [PATCH 18/63] Add code block copy and collapse functionality (#2361) * add buttons to codeblocks * add functionality * Document functions * Improve accessibility * Remove pointless DefaultReset * implement some requested changes * fix content shift when expanding or collapsing --------- Co-authored-by: Ajay Bura <32841439+ajbura@users.noreply.github.com> --- src/app/components/editor/Elements.tsx | 2 +- src/app/hooks/useTimeoutToggle.ts | 37 +++++++ src/app/plugins/react-custom-html-parser.tsx | 102 ++++++++++++++++--- src/app/styles/CustomHtml.css.ts | 29 +++++- 4 files changed, 155 insertions(+), 15 deletions(-) create mode 100644 src/app/hooks/useTimeoutToggle.ts diff --git a/src/app/components/editor/Elements.tsx b/src/app/components/editor/Elements.tsx index a7438ecd..6a6659b9 100644 --- a/src/app/components/editor/Elements.tsx +++ b/src/app/components/editor/Elements.tsx @@ -162,7 +162,7 @@ export function RenderElement({ attributes, element, children }: RenderElementPr visibility="Hover" hideTrack > -
{children}
+
{children}
); diff --git a/src/app/hooks/useTimeoutToggle.ts b/src/app/hooks/useTimeoutToggle.ts new file mode 100644 index 00000000..7eda99c1 --- /dev/null +++ b/src/app/hooks/useTimeoutToggle.ts @@ -0,0 +1,37 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; + +/** + * Temporarily sets a boolean state. + * + * @param duration - Duration in milliseconds before resetting (default: 1500) + * @param initial - Initial value (default: false) + */ +export function useTimeoutToggle(duration = 1500, initial = false): [boolean, () => void] { + const [active, setActive] = useState(initial); + const timeoutRef = useRef(null); + + const clear = () => { + if (timeoutRef.current !== null) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + }; + + const trigger = useCallback(() => { + setActive(!initial); + clear(); + timeoutRef.current = window.setTimeout(() => { + setActive(initial); + timeoutRef.current = null; + }, duration); + }, [duration, initial]); + + useEffect( + () => () => { + clear(); + }, + [] + ); + + return [active, trigger]; +} diff --git a/src/app/plugins/react-custom-html-parser.tsx b/src/app/plugins/react-custom-html-parser.tsx index cd683e36..04ebacd4 100644 --- a/src/app/plugins/react-custom-html-parser.tsx +++ b/src/app/plugins/react-custom-html-parser.tsx @@ -1,5 +1,13 @@ /* eslint-disable jsx-a11y/alt-text */ -import React, { ComponentPropsWithoutRef, ReactEventHandler, Suspense, lazy } from 'react'; +import React, { + ComponentPropsWithoutRef, + ReactEventHandler, + Suspense, + lazy, + useCallback, + useMemo, + useState, +} from 'react'; import { Element, Text as DOMText, @@ -9,10 +17,11 @@ import { } from 'html-react-parser'; import { MatrixClient } from 'matrix-js-sdk'; import classNames from 'classnames'; -import { Scroll, Text } from 'folds'; +import { Icon, IconButton, Icons, Scroll, Text } from 'folds'; import { IntermediateRepresentation, Opts as LinkifyOpts, OptFn } from 'linkifyjs'; import Linkify from 'linkify-react'; import { ErrorBoundary } from 'react-error-boundary'; +import { ChildNode } from 'domhandler'; import * as css from '../styles/CustomHtml.css'; import { getMxIdLocalPart, @@ -31,7 +40,8 @@ import { testMatrixTo, } from './matrix-to'; import { onEnterOrSpace } from '../utils/keyboard'; -import { tryDecodeURIComponent } from '../utils/dom'; +import { copyToClipboard, tryDecodeURIComponent } from '../utils/dom'; +import { useTimeoutToggle } from '../hooks/useTimeoutToggle'; const ReactPrism = lazy(() => import('./react-prism/ReactPrism')); @@ -195,6 +205,82 @@ export const highlightText = ( ); }); +export function CodeBlock(children: ChildNode[], opts: HTMLReactParserOptions) { + const LINE_LIMIT = 14; + + /** + * Recursively extracts and concatenates all text content from an array of ChildNode objects. + * + * @param {ChildNode[]} nodes - An array of ChildNode objects to extract text from. + * @returns {string} The concatenated plain text content of all descendant text nodes. + */ + const extractTextFromChildren = useCallback((nodes: ChildNode[]): string => { + let text = ''; + + nodes.forEach((node) => { + if (node.type === 'text') { + text += node.data; + } else if (node instanceof Element && node.children) { + text += extractTextFromChildren(node.children); + } + }); + + return text; + }, []); + + const [copied, setCopied] = useTimeoutToggle(); + const collapsible = useMemo( + () => extractTextFromChildren(children).split('\n').length > LINE_LIMIT, + [children, extractTextFromChildren] + ); + const [collapsed, setCollapsed] = useState(collapsible); + + const handleCopy = useCallback(() => { + copyToClipboard(extractTextFromChildren(children)); + setCopied(); + }, [children, extractTextFromChildren, setCopied]); + + const toggleCollapse = useCallback(() => { + setCollapsed((prev) => !prev); + }, []); + + return ( + <> +
+ + + + {collapsible && ( + + + + )} +
+ +
+ {domToReact(children, opts)} +
+
+ + ); +} + export const getReactCustomHtmlParser = ( mx: MatrixClient, roomId: string | undefined, @@ -271,15 +357,7 @@ export const getReactCustomHtmlParser = ( if (name === 'pre') { return ( - -
{domToReact(children, opts)}
-
+ {CodeBlock(children, opts)}
); } diff --git a/src/app/styles/CustomHtml.css.ts b/src/app/styles/CustomHtml.css.ts index d86a3236..ecbdbeee 100644 --- a/src/app/styles/CustomHtml.css.ts +++ b/src/app/styles/CustomHtml.css.ts @@ -85,10 +85,35 @@ export const CodeBlock = style([ MarginSpaced, { fontStyle: 'normal', + position: 'relative', }, ]); -export const CodeBlockInternal = style({ - padding: `${config.space.S200} ${config.space.S200} 0`, +export const CodeBlockInternal = recipe({ + base: { + padding: `${config.space.S200} ${config.space.S200} 0`, + minWidth: toRem(100), + }, + variants: { + collapsed: { + true: { + maxHeight: `calc(${config.lineHeight.T400} * 9.6)`, + }, + }, + }, +}); +export const CodeBlockControls = style({ + position: 'absolute', + top: config.space.S200, + right: config.space.S200, + visibility: 'hidden', + selectors: { + [`${CodeBlock}:hover &`]: { + visibility: 'visible', + }, + [`${CodeBlock}:focus-within &`]: { + visibility: 'visible', + }, + }, }); export const List = style([ From 9073dee9862c898dfdc206dbd6b301008c3bff83 Mon Sep 17 00:00:00 2001 From: Filipe Medeiros Date: Wed, 23 Jul 2025 16:17:17 +0100 Subject: [PATCH 19/63] Add button to start thread on reply (#2320) * add simple button to start a thread on reply * force build * remove useless actions * add actions back * change icon to ThreadPlus * add button to context menu * fix capital T --------- Co-authored-by: Ajay Bura <32841439+ajbura@users.noreply.github.com> --- src/app/features/room/RoomTimeline.tsx | 6 ++-- src/app/features/room/message/Message.tsx | 39 ++++++++++++++++++++++- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/app/features/room/RoomTimeline.tsx b/src/app/features/room/RoomTimeline.tsx index 773e115b..f2218b04 100644 --- a/src/app/features/room/RoomTimeline.tsx +++ b/src/app/features/room/RoomTimeline.tsx @@ -933,7 +933,7 @@ export function RoomTimeline({ ); const handleReplyClick: MouseEventHandler = useCallback( - (evt) => { + (evt, startThread = false) => { const replyId = evt.currentTarget.getAttribute('data-event-id'); if (!replyId) { console.warn('Button should have "data-event-id" attribute!'); @@ -944,7 +944,9 @@ export function RoomTimeline({ const editedReply = getEditedEvent(replyId, replyEvt, room.getUnfilteredTimelineSet()); const content: IContent = editedReply?.getContent()['m.new_content'] ?? replyEvt.getContent(); const { body, formatted_body: formattedBody } = content; - const { 'm.relates_to': relation } = replyEvt.getWireContent(); + const { 'm.relates_to': relation } = startThread + ? { 'm.relates_to': { rel_type: 'm.thread', event_id: replyId } } + : replyEvt.getWireContent(); const senderId = replyEvt.getSender(); if (senderId && typeof body === 'string') { setReplyDraft({ diff --git a/src/app/features/room/message/Message.tsx b/src/app/features/room/message/Message.tsx index b85605d5..c5de9ea1 100644 --- a/src/app/features/room/message/Message.tsx +++ b/src/app/features/room/message/Message.tsx @@ -669,7 +669,10 @@ export type MessageProps = { messageSpacing: MessageSpacing; onUserClick: MouseEventHandler; onUsernameClick: MouseEventHandler; - onReplyClick: MouseEventHandler; + onReplyClick: ( + ev: Parameters>[0], + startThread?: boolean + ) => void; onEditId?: (eventId?: string) => void; onReactionToggle: (targetEventId: string, key: string, shortcode?: string) => void; reply?: ReactNode; @@ -859,6 +862,8 @@ export const Message = as<'div', MessageProps>( }, 100); }; + const isThreadedMessage = mEvent.threadRootId !== undefined; + return ( ( > + {!isThreadedMessage && ( + onReplyClick(ev, true)} + data-event-id={mEvent.getId()} + variant="SurfaceVariant" + size="300" + radii="300" + > + + + )} {canEditEvent(mx, mEvent) && onEditId && ( onEditId(mEvent.getId())} @@ -1000,6 +1016,27 @@ export const Message = as<'div', MessageProps>( Reply + {!isThreadedMessage && ( + } + radii="300" + data-event-id={mEvent.getId()} + onClick={(evt: any) => { + onReplyClick(evt, true); + closeMenu(); + }} + > + + Reply in Thread + + + )} {canEditEvent(mx, mEvent) && onEditId && ( Date: Wed, 23 Jul 2025 20:59:32 +0530 Subject: [PATCH 20/63] Fix small height image half clickable view button (#2397) --- src/app/components/message/content/style.css.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/components/message/content/style.css.ts b/src/app/components/message/content/style.css.ts index f6cadd3c..93f3649c 100644 --- a/src/app/components/message/content/style.css.ts +++ b/src/app/components/message/content/style.css.ts @@ -16,6 +16,7 @@ export const AbsoluteContainer = style([ position: 'absolute', top: 0, left: 0, + zIndex: 1, width: '100%', height: '100%', }, From 67b05eeb09c8019fc67564f65ebca3d4b37653c6 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Wed, 23 Jul 2025 21:00:02 +0530 Subject: [PATCH 21/63] Render room avatar as fallback for dm group chat (#2398) * render room avatar for dm group chat * remove extra conditions --- src/app/utils/room.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/app/utils/room.ts b/src/app/utils/room.ts index cae23514..a962c45d 100644 --- a/src/app/utils/room.ts +++ b/src/app/utils/room.ts @@ -294,9 +294,14 @@ export const getDirectRoomAvatarUrl = ( useAuthentication = false ): string | undefined => { const mxcUrl = room.getAvatarFallbackMember()?.getMxcAvatarUrl(); - return mxcUrl - ? mx.mxcUrlToHttp(mxcUrl, size, size, 'crop', undefined, false, useAuthentication) ?? undefined - : undefined; + + if (!mxcUrl) { + return getRoomAvatarUrl(mx, room, size, useAuthentication); + } + + return ( + mx.mxcUrlToHttp(mxcUrl, size, size, 'crop', undefined, false, useAuthentication) ?? undefined + ); }; export const trimReplyFromBody = (body: string): string => { From 9183fd66b2dc50cc113d387ca246eb45b96c448d Mon Sep 17 00:00:00 2001 From: Gimle Larpes <97182804+GimleLarpes@users.noreply.github.com> Date: Sun, 27 Jul 2025 15:13:00 +0300 Subject: [PATCH 22/63] Add settings to enable 24-hour time format and customizable date format (#2347) * Add setting to enable 24-hour time format * added hour24Clock to TimeProps * Add incomplete dateFormatString setting * Move 24-hour toggle to Appearance * Add "Date & Time" subheading, cleanup after merge * Add setting for date formatting * Fix minor formatting and naming issues * Document functions * adress most comments * add hint for date formatting * add support for 24hr time to TimePicker * prevent overflow on small displays --- src/app/atoms/time/Time.jsx | 33 +- src/app/components/message/Time.tsx | 26 +- src/app/components/room-intro/RoomIntro.tsx | 6 +- src/app/components/time-date/TimePicker.tsx | 117 +++--- .../features/message-search/MessageSearch.tsx | 5 + .../message-search/SearchResultGroup.tsx | 10 +- src/app/features/room/RoomTimeline.tsx | 51 ++- .../features/room/jump-to-time/JumpToTime.tsx | 6 +- src/app/features/room/message/Message.tsx | 11 +- .../room/room-pin-menu/RoomPinMenu.tsx | 9 +- .../features/settings/devices/DeviceTile.tsx | 8 +- src/app/features/settings/general/General.tsx | 361 +++++++++++++++++- src/app/hooks/useDateFormat.ts | 34 ++ src/app/pages/client/inbox/Invites.tsx | 66 +++- src/app/pages/client/inbox/Notifications.tsx | 14 +- src/app/state/settings.ts | 7 + src/app/utils/time.ts | 9 +- 17 files changed, 691 insertions(+), 82 deletions(-) create mode 100644 src/app/hooks/useDateFormat.ts diff --git a/src/app/atoms/time/Time.jsx b/src/app/atoms/time/Time.jsx index 750b958f..d7bbe439 100644 --- a/src/app/atoms/time/Time.jsx +++ b/src/app/atoms/time/Time.jsx @@ -4,10 +4,25 @@ import PropTypes from 'prop-types'; import dateFormat from 'dateformat'; import { isInSameDay } from '../../../util/common'; -function Time({ timestamp, fullTime }) { +/** + * Renders a formatted timestamp. + * + * Displays the time in hour:minute format if the message is from today or yesterday, unless `fullTime` is true. + * For older messages, it shows the date and time. + * + * @param {number} timestamp - The timestamp to display. + * @param {boolean} [fullTime=false] - If true, always show the full date and time. + * @param {boolean} hour24Clock - Whether to use 24-hour time format. + * @param {string} dateFormatString - Format string for the date part. + * @returns {JSX.Element} A