Merge branch 'main' into terminal
1
.github/workflows/go.yml
vendored
|
@ -21,6 +21,7 @@ jobs:
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
sudo apt-get install libolm-dev libolm3 libgtk-3-dev libwebkit2gtk-4.1-dev libsoup-3.0-dev
|
sudo apt-get install libolm-dev libolm3 libgtk-3-dev libwebkit2gtk-4.1-dev libsoup-3.0-dev
|
||||||
go install golang.org/x/tools/cmd/goimports@latest
|
go install golang.org/x/tools/cmd/goimports@latest
|
||||||
go install honnef.co/go/tools/cmd/staticcheck@latest
|
go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
stages:
|
stages:
|
||||||
- frontend
|
- frontend
|
||||||
- build
|
- build
|
||||||
|
- build desktop
|
||||||
- docker
|
- docker
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -39,7 +40,7 @@ frontend:
|
||||||
- export MAUTRIX_VERSION=$(cat go.mod | grep 'maunium.net/go/mautrix ' | awk '{ print $2 }')
|
- export MAUTRIX_VERSION=$(cat go.mod | grep 'maunium.net/go/mautrix ' | awk '{ print $2 }')
|
||||||
- export GO_LDFLAGS="-s -w -linkmode external -extldflags -static -X go.mau.fi/gomuks/version.Tag=$CI_COMMIT_TAG -X go.mau.fi/gomuks/version.Commit=$CI_COMMIT_SHA -X 'go.mau.fi/gomuks/version.BuildTime=`date -Iseconds`' -X 'maunium.net/go/mautrix.GoModVersion=$MAUTRIX_VERSION'"
|
- export GO_LDFLAGS="-s -w -linkmode external -extldflags -static -X go.mau.fi/gomuks/version.Tag=$CI_COMMIT_TAG -X go.mau.fi/gomuks/version.Commit=$CI_COMMIT_SHA -X 'go.mau.fi/gomuks/version.BuildTime=`date -Iseconds`' -X 'maunium.net/go/mautrix.GoModVersion=$MAUTRIX_VERSION'"
|
||||||
script:
|
script:
|
||||||
- go build -ldflags "$GO_LDFLAGS" -o gomuks ./cmd/gomuks
|
- go build -ldflags "$GO_LDFLAGS" ./cmd/gomuks
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- gomuks
|
- gomuks
|
||||||
|
@ -81,6 +82,16 @@ linux/arm64:
|
||||||
- linux
|
- linux
|
||||||
- arm64
|
- arm64
|
||||||
|
|
||||||
|
windows/amd64:
|
||||||
|
<<: *build-linux
|
||||||
|
image: dock.mau.dev/tulir/gomuks-build-docker:windows-amd64
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- gomuks.exe
|
||||||
|
tags:
|
||||||
|
- linux
|
||||||
|
- amd64
|
||||||
|
|
||||||
macos/arm64:
|
macos/arm64:
|
||||||
stage: build
|
stage: build
|
||||||
tags:
|
tags:
|
||||||
|
@ -154,3 +165,89 @@ docker/manifest:
|
||||||
docker manifest create $MANIFEST_NAME $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64 $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-arm64
|
docker manifest create $MANIFEST_NAME $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64 $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-arm64
|
||||||
docker manifest push $MANIFEST_NAME
|
docker manifest push $MANIFEST_NAME
|
||||||
- docker rmi $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64 $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-arm64
|
- docker rmi $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64 $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-arm64
|
||||||
|
|
||||||
|
.build-desktop: &build-desktop
|
||||||
|
stage: build desktop
|
||||||
|
cache:
|
||||||
|
paths:
|
||||||
|
- .cache
|
||||||
|
before_script:
|
||||||
|
- mkdir -p .cache
|
||||||
|
- export GOPATH="$CI_PROJECT_DIR/.cache"
|
||||||
|
- export GOCACHE="$CI_PROJECT_DIR/.cache/build"
|
||||||
|
script:
|
||||||
|
- cd desktop
|
||||||
|
- wails3 task $PLATFORM:package
|
||||||
|
- ls bin
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- desktop/bin/*
|
||||||
|
dependencies:
|
||||||
|
- frontend
|
||||||
|
needs:
|
||||||
|
- frontend
|
||||||
|
|
||||||
|
desktop/linux/amd64:
|
||||||
|
<<: *build-desktop
|
||||||
|
image: dock.mau.dev/tulir/gomuks-build-docker/wails:linux-amd64
|
||||||
|
variables:
|
||||||
|
PLATFORM: linux
|
||||||
|
after_script:
|
||||||
|
- mv desktop/bin/gomuks-desktop .
|
||||||
|
- mv desktop/build/nfpm/bin/gomuks-desktop.deb .
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- gomuks-desktop
|
||||||
|
- gomuks-desktop.deb
|
||||||
|
tags:
|
||||||
|
- linux
|
||||||
|
- amd64
|
||||||
|
|
||||||
|
desktop/linux/arm64:
|
||||||
|
<<: *build-desktop
|
||||||
|
image: dock.mau.dev/tulir/gomuks-build-docker/wails:linux-arm64-native
|
||||||
|
variables:
|
||||||
|
PLATFORM: linux
|
||||||
|
after_script:
|
||||||
|
- mv desktop/bin/gomuks-desktop .
|
||||||
|
- mv desktop/build/nfpm/bin/gomuks-desktop.deb .
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- gomuks-desktop
|
||||||
|
- gomuks-desktop.deb
|
||||||
|
tags:
|
||||||
|
- linux
|
||||||
|
- arm64
|
||||||
|
|
||||||
|
desktop/windows/amd64:
|
||||||
|
<<: *build-desktop
|
||||||
|
image: dock.mau.dev/tulir/gomuks-build-docker/wails:windows-amd64
|
||||||
|
after_script:
|
||||||
|
- mv desktop/bin/gomuks-desktop.exe .
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- gomuks-desktop.exe
|
||||||
|
variables:
|
||||||
|
PLATFORM: windows
|
||||||
|
|
||||||
|
desktop/macos/arm64:
|
||||||
|
<<: *build-desktop
|
||||||
|
cache: {}
|
||||||
|
before_script:
|
||||||
|
- export PATH=/opt/homebrew/bin:/usr/local/bin:$PATH
|
||||||
|
- export LIBRARY_PATH=$(brew --prefix)/lib
|
||||||
|
- export CPATH=$(brew --prefix)/include
|
||||||
|
after_script:
|
||||||
|
- hdiutil create -srcFolder ./desktop/bin/gomuks-desktop.app/ -o ./gomuks-desktop.dmg
|
||||||
|
- codesign -s - --timestamp -i fi.mau.gomuks.desktop.mac gomuks-desktop.dmg
|
||||||
|
- mv desktop/bin/gomuks-desktop .
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- gomuks-desktop
|
||||||
|
# TODO generate proper dmgs
|
||||||
|
#- gomuks-desktop.dmg
|
||||||
|
variables:
|
||||||
|
PLATFORM: darwin
|
||||||
|
tags:
|
||||||
|
- macos
|
||||||
|
- arm64
|
||||||
|
|
|
@ -21,7 +21,7 @@ repos:
|
||||||
- id: go-staticcheck-repo-mod
|
- id: go-staticcheck-repo-mod
|
||||||
|
|
||||||
- repo: https://github.com/beeper/pre-commit-go
|
- repo: https://github.com/beeper/pre-commit-go
|
||||||
rev: v0.3.1
|
rev: v0.4.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: prevent-literal-http-methods
|
- id: prevent-literal-http-methods
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM alpine:3.20
|
FROM alpine:3.21
|
||||||
|
|
||||||
RUN apk add --no-cache ca-certificates jq curl
|
RUN apk add --no-cache ca-certificates jq curl
|
||||||
|
|
||||||
|
|
1
desktop/.gitignore
vendored
|
@ -1,2 +1,3 @@
|
||||||
.task
|
.task
|
||||||
bin
|
bin
|
||||||
|
build/appimage
|
||||||
|
|
|
@ -1,448 +1,54 @@
|
||||||
version: '3'
|
version: '3'
|
||||||
|
|
||||||
|
includes:
|
||||||
|
common: ./build/Taskfile.common.yml
|
||||||
|
windows: ./build/Taskfile.windows.yml
|
||||||
|
darwin: ./build/Taskfile.darwin.yml
|
||||||
|
linux: ./build/Taskfile.linux.yml
|
||||||
|
|
||||||
vars:
|
vars:
|
||||||
APP_NAME: "gomuks-desktop"
|
APP_NAME: "gomuks-desktop"
|
||||||
BIN_DIR: "bin"
|
BIN_DIR: "bin"
|
||||||
VITE_PORT: '{{.WAILS_VITE_PORT | default 9245}}'
|
VITE_PORT: '{{.WAILS_VITE_PORT | default 9245}}'
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
|
|
||||||
## -------------------------- Build -------------------------- ##
|
|
||||||
|
|
||||||
build:
|
build:
|
||||||
summary: Builds the application
|
summary: Builds the application
|
||||||
cmds:
|
cmds:
|
||||||
# Build for current OS
|
- task: "{{OS}}:build"
|
||||||
- task: build:{{OS}}
|
|
||||||
|
|
||||||
# Uncomment to build for specific OSes
|
|
||||||
# - task: build:linux
|
|
||||||
# - task: build:windows
|
|
||||||
# - task: build:darwin
|
|
||||||
|
|
||||||
|
|
||||||
## ------> Windows <-------
|
|
||||||
|
|
||||||
build:windows:
|
|
||||||
summary: Builds the application for Windows
|
|
||||||
deps:
|
|
||||||
- task: go:mod:tidy
|
|
||||||
- task: build:frontend
|
|
||||||
vars:
|
|
||||||
BUILD_FLAGS: '{{.BUILD_FLAGS}}'
|
|
||||||
- task: generate:icons
|
|
||||||
- task: generate:syso
|
|
||||||
vars:
|
|
||||||
ARCH: '{{.ARCH}}'
|
|
||||||
cmds:
|
|
||||||
- go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/gomuks-desktop.exe
|
|
||||||
vars:
|
|
||||||
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -ldflags="-w -s -H windowsgui"{{else}}-gcflags=all="-l"{{end}}'
|
|
||||||
env:
|
|
||||||
GOOS: windows
|
|
||||||
CGO_ENABLED: 0
|
|
||||||
GOARCH: '{{.ARCH | default ARCH}}'
|
|
||||||
PRODUCTION: '{{.PRODUCTION | default "false"}}'
|
|
||||||
|
|
||||||
build:windows:prod:arm64:
|
|
||||||
summary: Creates a production build of the application
|
|
||||||
cmds:
|
|
||||||
- task: build:windows
|
|
||||||
vars:
|
|
||||||
ARCH: arm64
|
|
||||||
PRODUCTION: "true"
|
|
||||||
|
|
||||||
build:windows:prod:amd64:
|
|
||||||
summary: Creates a production build of the application
|
|
||||||
cmds:
|
|
||||||
- task: build:windows
|
|
||||||
vars:
|
|
||||||
ARCH: amd64
|
|
||||||
PRODUCTION: "true"
|
|
||||||
|
|
||||||
build:windows:debug:arm64:
|
|
||||||
summary: Creates a debug build of the application
|
|
||||||
cmds:
|
|
||||||
- task: build:windows
|
|
||||||
vars:
|
|
||||||
ARCH: arm64
|
|
||||||
|
|
||||||
build:windows:debug:amd64:
|
|
||||||
summary: Creates a debug build of the application
|
|
||||||
cmds:
|
|
||||||
- task: build:windows
|
|
||||||
vars:
|
|
||||||
ARCH: amd64
|
|
||||||
|
|
||||||
## ------> Darwin <-------
|
|
||||||
|
|
||||||
build:darwin:
|
|
||||||
summary: Creates a production build of the application
|
|
||||||
deps:
|
|
||||||
- task: go:mod:tidy
|
|
||||||
- task: build:frontend
|
|
||||||
vars:
|
|
||||||
BUILD_FLAGS: '{{.BUILD_FLAGS}}'
|
|
||||||
- task: generate:icons
|
|
||||||
cmds:
|
|
||||||
- go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}
|
|
||||||
vars:
|
|
||||||
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -ldflags="-w -s"{{else}}-gcflags=all="-l"{{end}}'
|
|
||||||
env:
|
|
||||||
GOOS: darwin
|
|
||||||
CGO_ENABLED: 1
|
|
||||||
GOARCH: '{{.ARCH | default ARCH}}'
|
|
||||||
CGO_CFLAGS: "-mmacosx-version-min=10.15"
|
|
||||||
CGO_LDFLAGS: "-mmacosx-version-min=10.15"
|
|
||||||
MACOSX_DEPLOYMENT_TARGET: "10.15"
|
|
||||||
PRODUCTION: '{{.PRODUCTION | default "false"}}'
|
|
||||||
|
|
||||||
build:darwin:prod:arm64:
|
|
||||||
summary: Creates a production build of the application
|
|
||||||
cmds:
|
|
||||||
- task: build:darwin
|
|
||||||
vars:
|
|
||||||
ARCH: arm64
|
|
||||||
PRODUCTION: "true"
|
|
||||||
|
|
||||||
build:darwin:prod:amd64:
|
|
||||||
summary: Creates a production build of the application
|
|
||||||
cmds:
|
|
||||||
- task: build:darwin
|
|
||||||
vars:
|
|
||||||
ARCH: amd64
|
|
||||||
PRODUCTION: "true"
|
|
||||||
|
|
||||||
build:darwin:debug:arm64:
|
|
||||||
summary: Creates a debug build of the application
|
|
||||||
cmds:
|
|
||||||
- task: build:darwin
|
|
||||||
vars:
|
|
||||||
ARCH: arm64
|
|
||||||
|
|
||||||
build:darwin:debug:amd64:
|
|
||||||
summary: Creates a debug build of the application
|
|
||||||
cmds:
|
|
||||||
- task: build:darwin
|
|
||||||
vars:
|
|
||||||
ARCH: amd64
|
|
||||||
|
|
||||||
|
|
||||||
## ------> Linux <-------
|
|
||||||
|
|
||||||
build:linux:
|
|
||||||
summary: Builds the application for Linux
|
|
||||||
deps:
|
|
||||||
- task: go:mod:tidy
|
|
||||||
- task: build:frontend
|
|
||||||
vars:
|
|
||||||
BUILD_FLAGS: '{{.BUILD_FLAGS}}'
|
|
||||||
- task: generate:icons
|
|
||||||
vars:
|
|
||||||
ARCH: '{{.ARCH}}'
|
|
||||||
cmds:
|
|
||||||
- go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/gomuks-desktop
|
|
||||||
vars:
|
|
||||||
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -ldflags="-w -s"{{else}}-gcflags=all="-l"{{end}}'
|
|
||||||
env:
|
|
||||||
GOOS: linux
|
|
||||||
CGO_ENABLED: 1
|
|
||||||
GOARCH: '{{.ARCH | default ARCH}}'
|
|
||||||
PRODUCTION: '{{.PRODUCTION | default "false"}}'
|
|
||||||
|
|
||||||
build:linux:prod:arm64:
|
|
||||||
summary: Creates a production build of the application
|
|
||||||
cmds:
|
|
||||||
- task: build:linux
|
|
||||||
vars:
|
|
||||||
ARCH: arm64
|
|
||||||
PRODUCTION: "true"
|
|
||||||
|
|
||||||
build:linux:prod:amd64:
|
|
||||||
summary: Creates a production build of the application
|
|
||||||
cmds:
|
|
||||||
- task: build:linux
|
|
||||||
vars:
|
|
||||||
ARCH: amd64
|
|
||||||
PRODUCTION: "true"
|
|
||||||
|
|
||||||
build:linux:debug:arm64:
|
|
||||||
summary: Creates a debug build of the application
|
|
||||||
cmds:
|
|
||||||
- task: build:linux
|
|
||||||
vars:
|
|
||||||
ARCH: arm64
|
|
||||||
|
|
||||||
build:linux:debug:amd64:
|
|
||||||
summary: Creates a debug build of the application
|
|
||||||
cmds:
|
|
||||||
- task: build:linux
|
|
||||||
vars:
|
|
||||||
ARCH: amd64
|
|
||||||
|
|
||||||
## -------------------------- Package -------------------------- ##
|
|
||||||
|
|
||||||
package:
|
package:
|
||||||
summary: Packages a production build of the application into a bundle
|
summary: Packages a production build of the application
|
||||||
cmds:
|
cmds:
|
||||||
|
- task: "{{OS}}:package"
|
||||||
# Package for current OS
|
|
||||||
- task: package:{{OS}}
|
|
||||||
|
|
||||||
# Package for specific os/arch
|
|
||||||
# - task: package:darwin:arm64
|
|
||||||
# - task: package:darwin:amd64
|
|
||||||
# - task: package:windows:arm64
|
|
||||||
# - task: package:windows:amd64
|
|
||||||
|
|
||||||
## ------> Windows <------
|
|
||||||
|
|
||||||
package:windows:
|
|
||||||
summary: Packages a production build of the application into a `.exe` bundle
|
|
||||||
cmds:
|
|
||||||
- task: create:nsis:installer
|
|
||||||
vars:
|
|
||||||
ARCH: '{{.ARCH}}'
|
|
||||||
vars:
|
|
||||||
ARCH: '{{.ARCH | default ARCH}}'
|
|
||||||
|
|
||||||
package:windows:arm64:
|
|
||||||
summary: Packages a production build of the application into a `.exe` bundle
|
|
||||||
cmds:
|
|
||||||
- task: package:windows
|
|
||||||
vars:
|
|
||||||
ARCH: arm64
|
|
||||||
|
|
||||||
package:windows:amd64:
|
|
||||||
summary: Packages a production build of the application into a `.exe` bundle
|
|
||||||
cmds:
|
|
||||||
- task: package:windows
|
|
||||||
vars:
|
|
||||||
ARCH: amd64
|
|
||||||
|
|
||||||
generate:syso:
|
|
||||||
summary: Generates Windows `.syso` file
|
|
||||||
dir: build
|
|
||||||
cmds:
|
|
||||||
- wails3 generate syso -arch {{.ARCH}} -icon icon.ico -manifest wails.exe.manifest -info info.json -out ../wails.syso
|
|
||||||
vars:
|
|
||||||
ARCH: '{{.ARCH | default ARCH}}'
|
|
||||||
|
|
||||||
create:nsis:installer:
|
|
||||||
summary: Creates an NSIS installer
|
|
||||||
label: "NSIS Installer ({{.ARCH}})"
|
|
||||||
dir: build/nsis
|
|
||||||
sources:
|
|
||||||
- "{{.ROOT_DIR}}\\bin\\{{.APP_NAME}}.exe"
|
|
||||||
generates:
|
|
||||||
- "{{.ROOT_DIR}}\\bin\\{{.APP_NAME}}-{{.ARCH}}-installer.exe"
|
|
||||||
deps:
|
|
||||||
- task: build:windows
|
|
||||||
vars:
|
|
||||||
PRODUCTION: "true"
|
|
||||||
ARCH: '{{.ARCH}}'
|
|
||||||
cmds:
|
|
||||||
- makensis -DARG_WAILS_'{{.ARG_FLAG}}'_BINARY="{{.ROOT_DIR}}\{{.BIN_DIR}}\{{.APP_NAME}}.exe" project.nsi
|
|
||||||
vars:
|
|
||||||
ARCH: '{{.ARCH | default ARCH}}'
|
|
||||||
ARG_FLAG: '{{if eq .ARCH "amd64"}}AMD64{{else}}ARM64{{end}}'
|
|
||||||
|
|
||||||
## ------> Darwin <------
|
|
||||||
|
|
||||||
package:darwin:
|
|
||||||
summary: Packages a production build of the application into a `.app` bundle
|
|
||||||
platforms: [ darwin ]
|
|
||||||
deps:
|
|
||||||
- task: build:darwin
|
|
||||||
vars:
|
|
||||||
PRODUCTION: "true"
|
|
||||||
cmds:
|
|
||||||
- task: create:app:bundle
|
|
||||||
|
|
||||||
package:darwin:arm64:
|
|
||||||
summary: Packages a production build of the application into a `.app` bundle
|
|
||||||
platforms: [ darwin/arm64 ]
|
|
||||||
deps:
|
|
||||||
- task: package:darwin
|
|
||||||
vars:
|
|
||||||
ARCH: arm64
|
|
||||||
|
|
||||||
package:darwin:amd64:
|
|
||||||
summary: Packages a production build of the application into a `.app` bundle
|
|
||||||
platforms: [ darwin/amd64 ]
|
|
||||||
deps:
|
|
||||||
- task: package:darwin
|
|
||||||
vars:
|
|
||||||
ARCH: amd64
|
|
||||||
|
|
||||||
create:app:bundle:
|
|
||||||
summary: Creates an `.app` bundle
|
|
||||||
cmds:
|
|
||||||
- mkdir -p {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/{MacOS,Resources}
|
|
||||||
- cp build/icons.icns {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources
|
|
||||||
- cp {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS
|
|
||||||
- cp build/Info.plist {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents
|
|
||||||
|
|
||||||
## ------> Linux <------
|
|
||||||
|
|
||||||
package:linux:
|
|
||||||
summary: Packages a production build of the application for Linux
|
|
||||||
platforms: [ linux ]
|
|
||||||
deps:
|
|
||||||
- task: build:linux
|
|
||||||
vars:
|
|
||||||
PRODUCTION: "true"
|
|
||||||
cmds:
|
|
||||||
- task: create:appimage
|
|
||||||
|
|
||||||
create:appimage:
|
|
||||||
summary: Creates an AppImage
|
|
||||||
dir: build/appimage
|
|
||||||
platforms: [ linux ]
|
|
||||||
deps:
|
|
||||||
- task: build:linux
|
|
||||||
vars:
|
|
||||||
PRODUCTION: "true"
|
|
||||||
- task: generate:linux:dotdesktop
|
|
||||||
cmds:
|
|
||||||
# Copy binary + icon to appimage dir
|
|
||||||
- cp {{.APP_BINARY}} {{.APP_NAME}}
|
|
||||||
- cp ../appicon.png appicon.png
|
|
||||||
# Generate AppImage
|
|
||||||
- wails3 generate appimage -binary {{.APP_NAME}} -icon {{.ICON}} -desktopfile {{.DESKTOP_FILE}} -outputdir {{.OUTPUT_DIR}} -builddir {{.ROOT_DIR}}/build/appimage
|
|
||||||
vars:
|
|
||||||
APP_NAME: '{{.APP_NAME}}'
|
|
||||||
APP_BINARY: '../../bin/{{.APP_NAME}}'
|
|
||||||
ICON: '../appicon.png'
|
|
||||||
DESKTOP_FILE: '{{.APP_NAME}}.desktop'
|
|
||||||
OUTPUT_DIR: '../../bin'
|
|
||||||
|
|
||||||
generate:linux:dotdesktop:
|
|
||||||
summary: Generates a `.desktop` file
|
|
||||||
dir: build
|
|
||||||
sources:
|
|
||||||
- "appicon.png"
|
|
||||||
generates:
|
|
||||||
- '{{.ROOT_DIR}}/build/appimage/{{.APP_NAME}}.desktop'
|
|
||||||
cmds:
|
|
||||||
- mkdir -p {{.ROOT_DIR}}/build/appimage
|
|
||||||
# Run `wails3 generate .desktop -help` for all the options
|
|
||||||
- wails3 generate .desktop -name "{{.APP_NAME}}" -exec "{{.EXEC}}" -icon "{{.ICON}}" -outputfile {{.ROOT_DIR}}/build/appimage/{{.APP_NAME}}.desktop -categories "{{.CATEGORIES}}"
|
|
||||||
# -comment "A comment"
|
|
||||||
# -terminal "true"
|
|
||||||
# -version "1.0"
|
|
||||||
# -genericname "Generic Name"
|
|
||||||
# -keywords "keyword1;keyword2;"
|
|
||||||
# -startupnotify "true"
|
|
||||||
# -mimetype "application/x-extension1;application/x-extension2;"
|
|
||||||
|
|
||||||
vars:
|
|
||||||
APP_NAME: '{{.APP_NAME}}'
|
|
||||||
EXEC: '{{.APP_NAME}}'
|
|
||||||
ICON: 'appicon'
|
|
||||||
CATEGORIES: 'Development;'
|
|
||||||
OUTPUTFILE: '{{.ROOT_DIR}}/build/appimage/{{.APP_NAME}}.desktop'
|
|
||||||
|
|
||||||
## -------------------------- Misc -------------------------- ##
|
|
||||||
|
|
||||||
|
|
||||||
generate:icons:
|
|
||||||
summary: Generates Windows `.ico` and Mac `.icns` files from an image
|
|
||||||
dir: build
|
|
||||||
sources:
|
|
||||||
- "appicon.png"
|
|
||||||
generates:
|
|
||||||
- "icons.icns"
|
|
||||||
- "icons.ico"
|
|
||||||
cmds:
|
|
||||||
# Generates both .ico and .icns files
|
|
||||||
- wails3 generate icons -input appicon.png
|
|
||||||
|
|
||||||
install:frontend:deps:
|
|
||||||
summary: Install frontend dependencies
|
|
||||||
dir: ../web
|
|
||||||
sources:
|
|
||||||
- package.json
|
|
||||||
- package-lock.json
|
|
||||||
generates:
|
|
||||||
- node_modules/*
|
|
||||||
preconditions:
|
|
||||||
- sh: npm version
|
|
||||||
msg: "Looks like npm isn't installed. Npm is part of the Node installer: https://nodejs.org/en/download/"
|
|
||||||
cmds:
|
|
||||||
- npm install --silent --no-progress
|
|
||||||
|
|
||||||
build:frontend:
|
|
||||||
summary: Build the frontend project
|
|
||||||
dir: ../web
|
|
||||||
sources:
|
|
||||||
- "**/*"
|
|
||||||
generates:
|
|
||||||
- dist/*
|
|
||||||
deps:
|
|
||||||
- install:frontend:deps
|
|
||||||
- task: generate:bindings
|
|
||||||
vars:
|
|
||||||
BUILD_FLAGS: '{{.BUILD_FLAGS}}'
|
|
||||||
cmds:
|
|
||||||
- npm run build -q
|
|
||||||
|
|
||||||
generate:bindings:
|
|
||||||
summary: Generates bindings for the frontend
|
|
||||||
sources:
|
|
||||||
- "**/*.go"
|
|
||||||
- go.mod
|
|
||||||
- go.sum
|
|
||||||
generates: []
|
|
||||||
#- "../web/src/wails/**/*"
|
|
||||||
cmds: []
|
|
||||||
# For a complete list of options, run `wails3 generate bindings -help`
|
|
||||||
#- wails3 generate bindings -d ../web/src/wails -f '{{.BUILD_FLAGS}}'
|
|
||||||
|
|
||||||
go:mod:tidy:
|
|
||||||
summary: Runs `go mod tidy`
|
|
||||||
internal: true
|
|
||||||
generates:
|
|
||||||
- go.sum
|
|
||||||
sources:
|
|
||||||
- go.mod
|
|
||||||
cmds:
|
|
||||||
- go mod tidy
|
|
||||||
|
|
||||||
# ----------------------- dev ----------------------- #
|
|
||||||
|
|
||||||
|
|
||||||
run:
|
run:
|
||||||
summary: Runs the application
|
summary: Runs the application
|
||||||
cmds:
|
cmds:
|
||||||
- task: run:{{OS}}
|
- task: "{{OS}}:run"
|
||||||
|
|
||||||
run:windows:
|
|
||||||
cmds:
|
|
||||||
- '{{.BIN_DIR}}\\{{.APP_NAME}}.exe'
|
|
||||||
|
|
||||||
run:linux:
|
|
||||||
cmds:
|
|
||||||
- '{{.BIN_DIR}}/{{.APP_NAME}}'
|
|
||||||
|
|
||||||
run:darwin:
|
|
||||||
cmds:
|
|
||||||
- '{{.BIN_DIR}}/{{.APP_NAME}}'
|
|
||||||
|
|
||||||
dev:frontend:
|
|
||||||
summary: Runs the frontend in development mode
|
|
||||||
dir: ../web
|
|
||||||
deps:
|
|
||||||
- task: install:frontend:deps
|
|
||||||
cmds:
|
|
||||||
- npm run dev -- --port {{.VITE_PORT}} --strictPort
|
|
||||||
|
|
||||||
dev:
|
dev:
|
||||||
summary: Runs the application in development mode
|
summary: Runs the application in development mode
|
||||||
cmds:
|
cmds:
|
||||||
- wails3 dev -config ./build/devmode.config.yaml -port {{.VITE_PORT}}
|
- wails3 dev -config ./build/config.yml -port {{.VITE_PORT}}
|
||||||
|
|
||||||
dev:reload:
|
darwin:build:universal:
|
||||||
summary: Reloads the application
|
summary: Builds darwin universal binary (arm64 + amd64)
|
||||||
cmds:
|
cmds:
|
||||||
- task: run
|
- task: darwin:build
|
||||||
|
vars:
|
||||||
|
ARCH: amd64
|
||||||
|
- mv {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}-amd64
|
||||||
|
- task: darwin:build
|
||||||
|
vars:
|
||||||
|
ARCH: arm64
|
||||||
|
- mv {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}-arm64
|
||||||
|
- lipo -create -output {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}-amd64 {{.BIN_DIR}}/{{.APP_NAME}}-arm64
|
||||||
|
- rm {{.BIN_DIR}}/{{.APP_NAME}}-amd64 {{.BIN_DIR}}/{{.APP_NAME}}-arm64
|
||||||
|
|
||||||
|
darwin:package:universal:
|
||||||
|
summary: Packages darwin universal binary (arm64 + amd64)
|
||||||
|
deps:
|
||||||
|
- darwin:build:universal
|
||||||
|
cmds:
|
||||||
|
- task: darwin:create:app:bundle
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
<key>NSHighResolutionCapable</key>
|
<key>NSHighResolutionCapable</key>
|
||||||
<string>true</string>
|
<string>true</string>
|
||||||
<key>NSHumanReadableCopyright</key>
|
<key>NSHumanReadableCopyright</key>
|
||||||
<string>© 2024, Tulir Asokan</string>
|
<string>© 2024, gomuks authors</string>
|
||||||
<key>NSAppTransportSecurity</key>
|
<key>NSAppTransportSecurity</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSAllowsLocalNetworking</key>
|
<key>NSAllowsLocalNetworking</key>
|
||||||
|
|
|
@ -22,6 +22,6 @@
|
||||||
<key>NSHighResolutionCapable</key>
|
<key>NSHighResolutionCapable</key>
|
||||||
<string>true</string>
|
<string>true</string>
|
||||||
<key>NSHumanReadableCopyright</key>
|
<key>NSHumanReadableCopyright</key>
|
||||||
<string>© 2024, Tulir Asokan</string>
|
<string>© 2024, gomuks authors</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
75
desktop/build/Taskfile.common.yml
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
go:mod:tidy:
|
||||||
|
summary: Runs `go mod tidy`
|
||||||
|
internal: true
|
||||||
|
generates:
|
||||||
|
- go.sum
|
||||||
|
sources:
|
||||||
|
- go.mod
|
||||||
|
cmds:
|
||||||
|
- go mod tidy
|
||||||
|
|
||||||
|
install:frontend:deps:
|
||||||
|
summary: Install frontend dependencies
|
||||||
|
dir: ../web
|
||||||
|
sources:
|
||||||
|
- package.json
|
||||||
|
- package-lock.json
|
||||||
|
generates:
|
||||||
|
- node_modules/*
|
||||||
|
preconditions:
|
||||||
|
- sh: npm version
|
||||||
|
msg: "Looks like npm isn't installed. Npm is part of the Node installer: https://nodejs.org/en/download/"
|
||||||
|
cmds:
|
||||||
|
- npm install
|
||||||
|
|
||||||
|
build:frontend:
|
||||||
|
summary: Build the frontend project
|
||||||
|
dir: ../web
|
||||||
|
sources:
|
||||||
|
- "**/*"
|
||||||
|
generates:
|
||||||
|
- dist/*
|
||||||
|
deps:
|
||||||
|
- task: install:frontend:deps
|
||||||
|
#- task: generate:bindings
|
||||||
|
cmds:
|
||||||
|
- npm run build -q
|
||||||
|
|
||||||
|
generate:bindings:
|
||||||
|
summary: Generates bindings for the frontend
|
||||||
|
sources:
|
||||||
|
- "**/*.go"
|
||||||
|
- go.mod
|
||||||
|
- go.sum
|
||||||
|
generates: []
|
||||||
|
#- "frontend/bindings/**/*"
|
||||||
|
cmds: []
|
||||||
|
#- wails3 generate bindings -f '{{.BUILD_FLAGS}}'{{if .UseTypescript}} -ts{{end}}
|
||||||
|
|
||||||
|
generate:icons:
|
||||||
|
summary: Generates Windows `.ico` and Mac `.icns` files from an image
|
||||||
|
dir: build
|
||||||
|
sources:
|
||||||
|
- "appicon.png"
|
||||||
|
generates:
|
||||||
|
- "icons.icns"
|
||||||
|
- "icons.ico"
|
||||||
|
cmds:
|
||||||
|
- wails3 generate icons -input appicon.png
|
||||||
|
|
||||||
|
dev:frontend:
|
||||||
|
summary: Runs the frontend in development mode
|
||||||
|
dir: ../web
|
||||||
|
deps:
|
||||||
|
- task: install:frontend:deps
|
||||||
|
cmds:
|
||||||
|
- npm run dev -- --port {{.VITE_PORT}} --strictPort
|
||||||
|
|
||||||
|
update:build-assets:
|
||||||
|
summary: Updates the build assets
|
||||||
|
dir: build
|
||||||
|
cmds:
|
||||||
|
- wails3 update build-assets -name "{{.APP_NAME}}" -binaryname "{{.APP_NAME}}" -config config.yml -dir .
|
47
desktop/build/Taskfile.darwin.yml
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
includes:
|
||||||
|
common: Taskfile.common.yml
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
build:
|
||||||
|
summary: Creates a production build of the application
|
||||||
|
deps: []
|
||||||
|
#- task: common:go:mod:tidy
|
||||||
|
#- task: common:build:frontend
|
||||||
|
#- task: common:generate:icons
|
||||||
|
cmds:
|
||||||
|
- MAUTRIX_VERSION=$(cat go.mod | grep 'maunium.net/go/mautrix ' | awk '{ print $2 }')
|
||||||
|
- GO_LDFLAGS="-s -w -X go.mau.fi/gomuks/version.Tag=$CI_COMMIT_TAG -X go.mau.fi/gomuks/version.Commit=$CI_COMMIT_SHA -X 'go.mau.fi/gomuks/version.BuildTime=`date -Iseconds`' -X 'maunium.net/go/mautrix.GoModVersion=$MAUTRIX_VERSION'"
|
||||||
|
- go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}
|
||||||
|
vars:
|
||||||
|
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath{{else}}-gcflags=all="-l"{{end}}'
|
||||||
|
env:
|
||||||
|
GOOS: darwin
|
||||||
|
CGO_ENABLED: 1
|
||||||
|
GOARCH: '{{.ARCH | default ARCH}}'
|
||||||
|
CGO_CFLAGS: "-mmacosx-version-min=11.0"
|
||||||
|
CGO_LDFLAGS: "-mmacosx-version-min=11.0"
|
||||||
|
MACOSX_DEPLOYMENT_TARGET: "11.0"
|
||||||
|
PRODUCTION: '{{.PRODUCTION | default "false"}}'
|
||||||
|
|
||||||
|
package:
|
||||||
|
summary: Packages a production build of the application into a `.app` bundle
|
||||||
|
deps:
|
||||||
|
- task: build
|
||||||
|
vars:
|
||||||
|
PRODUCTION: "true"
|
||||||
|
cmds:
|
||||||
|
- task: create:app:bundle
|
||||||
|
|
||||||
|
create:app:bundle:
|
||||||
|
summary: Creates an `.app` bundle
|
||||||
|
cmds:
|
||||||
|
- mkdir -p {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/{MacOS,Resources}
|
||||||
|
- cp build/icons.icns {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources
|
||||||
|
- cp {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS
|
||||||
|
- cp build/Info.plist {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents
|
||||||
|
|
||||||
|
run:
|
||||||
|
cmds:
|
||||||
|
- '{{.BIN_DIR}}/{{.APP_NAME}}'
|
117
desktop/build/Taskfile.linux.yml
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
includes:
|
||||||
|
common: Taskfile.common.yml
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
build:
|
||||||
|
summary: Builds the application for Linux
|
||||||
|
deps: []
|
||||||
|
#- task: common:go:mod:tidy
|
||||||
|
#- task: common:build:frontend
|
||||||
|
#- task: common:generate:icons
|
||||||
|
cmds:
|
||||||
|
- MAUTRIX_VERSION=$(cat go.mod | grep 'maunium.net/go/mautrix ' | awk '{ print $2 }')
|
||||||
|
- GO_LDFLAGS="-s -w -X go.mau.fi/gomuks/version.Tag=$CI_COMMIT_TAG -X go.mau.fi/gomuks/version.Commit=$CI_COMMIT_SHA -X 'go.mau.fi/gomuks/version.BuildTime=`date -Iseconds`' -X 'maunium.net/go/mautrix.GoModVersion=$MAUTRIX_VERSION'"
|
||||||
|
- go build {{.BUILD_FLAGS}} -ldflags "$GO_LDFLAGS" -o {{.BIN_DIR}}/{{.APP_NAME}}
|
||||||
|
vars:
|
||||||
|
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath{{else}}-gcflags=all="-l"{{end}}'
|
||||||
|
env:
|
||||||
|
GOOS: linux
|
||||||
|
CGO_ENABLED: 1
|
||||||
|
GOARCH: '{{.ARCH | default ARCH}}'
|
||||||
|
PRODUCTION: '{{.PRODUCTION | default "false"}}'
|
||||||
|
|
||||||
|
package:
|
||||||
|
summary: Packages a production build of the application for Linux
|
||||||
|
deps:
|
||||||
|
- task: build
|
||||||
|
vars:
|
||||||
|
PRODUCTION: "true"
|
||||||
|
cmds:
|
||||||
|
#- task: create:appimage
|
||||||
|
- task: create:deb
|
||||||
|
#- task: create:rpm
|
||||||
|
#- task: create:aur
|
||||||
|
|
||||||
|
create:appimage:
|
||||||
|
summary: Creates an AppImage
|
||||||
|
dir: build/appimage
|
||||||
|
deps:
|
||||||
|
- task: build
|
||||||
|
vars:
|
||||||
|
PRODUCTION: "true"
|
||||||
|
- task: generate:dotdesktop
|
||||||
|
cmds:
|
||||||
|
- cp {{.APP_BINARY}} {{.APP_NAME}}
|
||||||
|
- cp ../appicon.png appicon.png
|
||||||
|
- wails3 generate appimage -binary {{.APP_NAME}} -icon {{.ICON}} -desktopfile {{.DESKTOP_FILE}} -outputdir {{.OUTPUT_DIR}} -builddir {{.ROOT_DIR}}/build/appimage
|
||||||
|
vars:
|
||||||
|
APP_NAME: '{{.APP_NAME}}'
|
||||||
|
APP_BINARY: '../../bin/{{.APP_NAME}}'
|
||||||
|
ICON: '../appicon.png'
|
||||||
|
DESKTOP_FILE: '{{.APP_NAME}}.desktop'
|
||||||
|
OUTPUT_DIR: '../../bin'
|
||||||
|
|
||||||
|
create:deb:
|
||||||
|
summary: Creates a deb package
|
||||||
|
deps:
|
||||||
|
- task: build
|
||||||
|
vars:
|
||||||
|
PRODUCTION: "true"
|
||||||
|
cmds:
|
||||||
|
- task: generate:dotdesktop
|
||||||
|
- task: generate:deb
|
||||||
|
|
||||||
|
create:rpm:
|
||||||
|
summary: Creates a rpm package
|
||||||
|
deps:
|
||||||
|
- task: build
|
||||||
|
vars:
|
||||||
|
PRODUCTION: "true"
|
||||||
|
cmds:
|
||||||
|
- task: generate:dotdesktop
|
||||||
|
- task: generate:rpm
|
||||||
|
|
||||||
|
create:aur:
|
||||||
|
summary: Creates a arch linux packager package
|
||||||
|
deps:
|
||||||
|
- task: build
|
||||||
|
vars:
|
||||||
|
PRODUCTION: "true"
|
||||||
|
cmds:
|
||||||
|
- task: generate:dotdesktop
|
||||||
|
- task: generate:aur
|
||||||
|
|
||||||
|
generate:deb:
|
||||||
|
summary: Creates a deb package
|
||||||
|
cmds:
|
||||||
|
- wails3 tool package -name {{.APP_NAME}} -format deb -config ./build/nfpm/nfpm.yaml
|
||||||
|
|
||||||
|
generate:rpm:
|
||||||
|
summary: Creates a rpm package
|
||||||
|
cmds:
|
||||||
|
- wails3 tool package -name {{.APP_NAME}} -format rpm -config ./build/nfpm/nfpm.yaml
|
||||||
|
|
||||||
|
generate:aur:
|
||||||
|
summary: Creates a arch linux packager package
|
||||||
|
cmds:
|
||||||
|
- wails3 tool package -name {{.APP_NAME}} -format arch -config ./build/nfpm/nfpm.yaml
|
||||||
|
|
||||||
|
generate:dotdesktop:
|
||||||
|
summary: Generates a `.desktop` file
|
||||||
|
dir: build
|
||||||
|
cmds:
|
||||||
|
- mkdir -p {{.ROOT_DIR}}/build/nfpm/bin
|
||||||
|
- wails3 generate .desktop -name "{{.APP_NAME}}" -exec "{{.EXEC}}" -icon "{{.ICON}}" -outputfile {{.ROOT_DIR}}/build/{{.APP_NAME}}.desktop -categories "{{.CATEGORIES}}"
|
||||||
|
- cp {{.ROOT_DIR}}/build/{{.APP_NAME}}.desktop {{.ROOT_DIR}}/build/nfpm/bin/{{.APP_NAME}}.desktop
|
||||||
|
vars:
|
||||||
|
APP_NAME: '{{.APP_NAME}}'
|
||||||
|
EXEC: '{{.APP_NAME}}'
|
||||||
|
ICON: 'appicon'
|
||||||
|
CATEGORIES: 'Network;InstantMessaging;Chat;'
|
||||||
|
OUTPUTFILE: '{{.ROOT_DIR}}/build/{{.APP_NAME}}.desktop'
|
||||||
|
|
||||||
|
run:
|
||||||
|
cmds:
|
||||||
|
- '{{.BIN_DIR}}/{{.APP_NAME}}'
|
62
desktop/build/Taskfile.windows.yml
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
includes:
|
||||||
|
common: Taskfile.common.yml
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
build:
|
||||||
|
summary: Builds the application for Windows
|
||||||
|
deps:
|
||||||
|
#- task: common:go:mod:tidy
|
||||||
|
#- task: common:build:frontend
|
||||||
|
#- task: common:generate:icons
|
||||||
|
- task: generate:syso
|
||||||
|
cmds:
|
||||||
|
- MAUTRIX_VERSION=$(cat go.mod | grep 'maunium.net/go/mautrix ' | awk '{ print $2 }')
|
||||||
|
- GO_LDFLAGS="-s -w -H windowsgui -X go.mau.fi/gomuks/version.Tag=$CI_COMMIT_TAG -X go.mau.fi/gomuks/version.Commit=$CI_COMMIT_SHA -X 'go.mau.fi/gomuks/version.BuildTime=`date -Iseconds`' -X 'maunium.net/go/mautrix.GoModVersion=$MAUTRIX_VERSION'"
|
||||||
|
- go build {{.BUILD_FLAGS}} -ldflags "$GO_LDFLAGS" -o {{.BIN_DIR}}/{{.APP_NAME}}.exe
|
||||||
|
- cmd: powershell Remove-item *.syso
|
||||||
|
platforms: [windows]
|
||||||
|
- cmd: rm -f *.syso
|
||||||
|
platforms: [linux, darwin]
|
||||||
|
vars:
|
||||||
|
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath{{else}}-gcflags=all="-l"{{end}}'
|
||||||
|
env:
|
||||||
|
GOOS: windows
|
||||||
|
CGO_ENABLED: 1
|
||||||
|
GOARCH: '{{.ARCH | default ARCH}}'
|
||||||
|
PRODUCTION: '{{.PRODUCTION | default "false"}}'
|
||||||
|
|
||||||
|
package:
|
||||||
|
summary: Packages a production build of the application into a `.exe` bundle
|
||||||
|
cmds:
|
||||||
|
- task: build
|
||||||
|
vars:
|
||||||
|
PRODUCTION: "true"
|
||||||
|
#cmds:
|
||||||
|
# - task: create:nsis:installer
|
||||||
|
|
||||||
|
generate:syso:
|
||||||
|
summary: Generates Windows `.syso` file
|
||||||
|
dir: build
|
||||||
|
cmds:
|
||||||
|
- wails3 generate syso -arch {{.ARCH}} -icon icon.ico -manifest wails.exe.manifest -info info.json -out ../wails_windows_{{.ARCH}}.syso
|
||||||
|
vars:
|
||||||
|
ARCH: '{{.ARCH | default ARCH}}'
|
||||||
|
|
||||||
|
create:nsis:installer:
|
||||||
|
summary: Creates an NSIS installer
|
||||||
|
dir: build/nsis
|
||||||
|
deps:
|
||||||
|
- task: build
|
||||||
|
vars:
|
||||||
|
PRODUCTION: "true"
|
||||||
|
cmds:
|
||||||
|
- makensis -DARG_WAILS_{{.ARG_FLAG}}_BINARY="{{.ROOT_DIR}}\{{.BIN_DIR}}\{{.APP_NAME}}.exe" project.nsi
|
||||||
|
vars:
|
||||||
|
ARCH: '{{.ARCH | default ARCH}}'
|
||||||
|
ARG_FLAG: '{{if eq .ARCH "amd64"}}AMD64{{else}}ARM64{{end}}'
|
||||||
|
|
||||||
|
run:
|
||||||
|
cmds:
|
||||||
|
- '{{.BIN_DIR}}\\{{.APP_NAME}}.exe'
|
60
desktop/build/config.yml
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
# This file contains the configuration for this project.
|
||||||
|
# When you update `info` or `fileAssociations`, run `wails3 task common:update:build-assets` to update the assets.
|
||||||
|
# Note that this will overwrite any changes you have made to the assets.
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
info:
|
||||||
|
companyName: ""
|
||||||
|
productName: "gomuks desktop"
|
||||||
|
productIdentifier: "fi.mau.gomuks.desktop"
|
||||||
|
description: "A Matrix client written in Go and React"
|
||||||
|
copyright: "© 2024, gomuks authors"
|
||||||
|
comments: ""
|
||||||
|
version: "0.4.0"
|
||||||
|
|
||||||
|
# Dev mode configuration
|
||||||
|
dev_mode:
|
||||||
|
root_path: .
|
||||||
|
log_level: warn
|
||||||
|
debounce: 1000
|
||||||
|
ignore:
|
||||||
|
dir:
|
||||||
|
- .git
|
||||||
|
- node_modules
|
||||||
|
- frontend
|
||||||
|
- bin
|
||||||
|
file:
|
||||||
|
- .DS_Store
|
||||||
|
- .gitignore
|
||||||
|
- .gitkeep
|
||||||
|
watched_extension:
|
||||||
|
- "*.go"
|
||||||
|
git_ignore: true
|
||||||
|
executes:
|
||||||
|
- cmd: wails3 task common:install:frontend:deps
|
||||||
|
type: once
|
||||||
|
- cmd: wails3 task common:dev:frontend
|
||||||
|
type: background
|
||||||
|
- cmd: go mod tidy
|
||||||
|
type: blocking
|
||||||
|
- cmd: wails3 task build
|
||||||
|
type: blocking
|
||||||
|
- cmd: wails3 task run
|
||||||
|
type: primary
|
||||||
|
|
||||||
|
# File Associations
|
||||||
|
# More information at: https://v3alpha.wails.io/noit/done/yet
|
||||||
|
fileAssociations:
|
||||||
|
# - ext: wails
|
||||||
|
# name: Wails
|
||||||
|
# description: Wails Application File
|
||||||
|
# iconName: wailsFileIcon
|
||||||
|
# role: Editor
|
||||||
|
# - ext: jpg
|
||||||
|
# name: JPEG
|
||||||
|
# description: Image File
|
||||||
|
# iconName: jpegFileIcon
|
||||||
|
# role: Editor
|
||||||
|
|
||||||
|
# Other data
|
||||||
|
other: []
|
|
@ -1,28 +0,0 @@
|
||||||
config:
|
|
||||||
root_path: .
|
|
||||||
log_level: warn
|
|
||||||
debounce: 1000
|
|
||||||
ignore:
|
|
||||||
dir:
|
|
||||||
- .git
|
|
||||||
- node_modules
|
|
||||||
- frontend
|
|
||||||
- bin
|
|
||||||
file:
|
|
||||||
- .DS_Store
|
|
||||||
- .gitignore
|
|
||||||
- .gitkeep
|
|
||||||
watched_extension:
|
|
||||||
- "*.go"
|
|
||||||
git_ignore: true
|
|
||||||
executes:
|
|
||||||
- cmd: wails3 task install:frontend:deps
|
|
||||||
type: once
|
|
||||||
- cmd: wails3 task dev:frontend
|
|
||||||
type: background
|
|
||||||
- cmd: go mod tidy
|
|
||||||
type: blocking
|
|
||||||
- cmd: wails3 task build
|
|
||||||
type: blocking
|
|
||||||
- cmd: wails3 task run
|
|
||||||
type: primary
|
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
@ -6,8 +6,8 @@
|
||||||
"0000": {
|
"0000": {
|
||||||
"ProductVersion": "0.4.0",
|
"ProductVersion": "0.4.0",
|
||||||
"CompanyName": "",
|
"CompanyName": "",
|
||||||
"FileDescription": "",
|
"FileDescription": "A Matrix client written in Go and React",
|
||||||
"LegalCopyright": "© 2024, Tulir Asokan",
|
"LegalCopyright": "© 2024, gomuks authors",
|
||||||
"ProductName": "gomuks desktop",
|
"ProductName": "gomuks desktop",
|
||||||
"Comments": ""
|
"Comments": ""
|
||||||
}
|
}
|
||||||
|
|
50
desktop/build/nfpm/nfpm.yaml
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
# Feel free to remove those if you don't want/need to use them.
|
||||||
|
# Make sure to check the documentation at https://nfpm.goreleaser.com
|
||||||
|
#
|
||||||
|
# The lines below are called `modelines`. See `:help modeline`
|
||||||
|
|
||||||
|
name: "gomuks-desktop"
|
||||||
|
arch: ${GOARCH}
|
||||||
|
platform: "linux"
|
||||||
|
version: "0.4.0"
|
||||||
|
section: "default"
|
||||||
|
priority: "extra"
|
||||||
|
maintainer: Tulir Asokan <tulir@maunium.net>
|
||||||
|
description: "A Matrix client written in Go and React"
|
||||||
|
vendor: ""
|
||||||
|
homepage: "https://wails.io"
|
||||||
|
license: "MIT"
|
||||||
|
release: "1"
|
||||||
|
|
||||||
|
contents:
|
||||||
|
- src: "./bin/gomuks-desktop"
|
||||||
|
dst: "/usr/local/bin/gomuks-desktop"
|
||||||
|
- src: "./build/appicon.png"
|
||||||
|
dst: "/usr/share/icons/hicolor/128x128/apps/gomuks-desktop.png"
|
||||||
|
- src: "./build/gomuks-desktop.desktop"
|
||||||
|
dst: "/usr/share/applications/gomuks-desktop.desktop"
|
||||||
|
|
||||||
|
depends:
|
||||||
|
- gtk3
|
||||||
|
- libwebkit2gtk
|
||||||
|
|
||||||
|
# replaces:
|
||||||
|
# - foobar
|
||||||
|
# provides:
|
||||||
|
# - bar
|
||||||
|
# depends:
|
||||||
|
# - gtk3
|
||||||
|
# - libwebkit2gtk
|
||||||
|
recommends:
|
||||||
|
- ffmpeg
|
||||||
|
# suggests:
|
||||||
|
# - something-else
|
||||||
|
# conflicts:
|
||||||
|
# - not-foo
|
||||||
|
# - not-bar
|
||||||
|
# changelog: "changelog.yaml"
|
||||||
|
# scripts:
|
||||||
|
# preinstall: ./build/nfpm/scripts/preinstall.sh
|
||||||
|
# postinstall: ./build/nfpm/scripts/postinstall.sh
|
||||||
|
# preremove: ./build/nfpm/scripts/preremove.sh
|
||||||
|
# postremove: ./build/nfpm/scripts/postremove.sh
|
1
desktop/build/nfpm/scripts/postinstall.sh
Normal file
|
@ -0,0 +1 @@
|
||||||
|
#!/bin/bash
|
1
desktop/build/nfpm/scripts/postremove.sh
Normal file
|
@ -0,0 +1 @@
|
||||||
|
#!/bin/bash
|
1
desktop/build/nfpm/scripts/preinstall.sh
Normal file
|
@ -0,0 +1 @@
|
||||||
|
#!/bin/bash
|
1
desktop/build/nfpm/scripts/preremove.sh
Normal file
|
@ -0,0 +1 @@
|
||||||
|
#!/bin/bash
|
|
@ -20,10 +20,10 @@ Unicode true
|
||||||
## The following information is taken from the wails_tools.nsh file, but they can be overwritten here.
|
## The following information is taken from the wails_tools.nsh file, but they can be overwritten here.
|
||||||
####
|
####
|
||||||
## !define INFO_PROJECTNAME "my-project" # Default "gomuks-desktop"
|
## !define INFO_PROJECTNAME "my-project" # Default "gomuks-desktop"
|
||||||
## !define INFO_COMPANYNAME "My Company" # Default "My Company"
|
## !define INFO_COMPANYNAME "My Company" # Default ""
|
||||||
## !define INFO_PRODUCTNAME "My Product Name" # Default "My Product"
|
## !define INFO_PRODUCTNAME "My Product Name" # Default "gomuks desktop"
|
||||||
## !define INFO_PRODUCTVERSION "1.0.0" # Default "0.1.0"
|
## !define INFO_PRODUCTVERSION "1.0.0" # Default "0.1.0"
|
||||||
## !define INFO_COPYRIGHT "(c) Now, My Company" # Default "© now, My Company"
|
## !define INFO_COPYRIGHT "(c) Now, My Company" # Default "© gomuks authors"
|
||||||
###
|
###
|
||||||
## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe"
|
## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe"
|
||||||
## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
|
## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
|
||||||
|
@ -91,6 +91,8 @@ Section
|
||||||
CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||||
CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||||
|
|
||||||
|
!insertmacro wails.associateFiles
|
||||||
|
|
||||||
!insertmacro wails.writeUninstaller
|
!insertmacro wails.writeUninstaller
|
||||||
SectionEnd
|
SectionEnd
|
||||||
|
|
||||||
|
@ -104,5 +106,7 @@ Section "uninstall"
|
||||||
Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk"
|
Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk"
|
||||||
Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"
|
Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"
|
||||||
|
|
||||||
|
!insertmacro wails.unassociateFiles
|
||||||
|
|
||||||
!insertmacro wails.deleteUninstaller
|
!insertmacro wails.deleteUninstaller
|
||||||
SectionEnd
|
SectionEnd
|
||||||
|
|
|
@ -8,16 +8,16 @@
|
||||||
!define INFO_PROJECTNAME "gomuks-desktop"
|
!define INFO_PROJECTNAME "gomuks-desktop"
|
||||||
!endif
|
!endif
|
||||||
!ifndef INFO_COMPANYNAME
|
!ifndef INFO_COMPANYNAME
|
||||||
!define INFO_COMPANYNAME "My Company"
|
!define INFO_COMPANYNAME ""
|
||||||
!endif
|
!endif
|
||||||
!ifndef INFO_PRODUCTNAME
|
!ifndef INFO_PRODUCTNAME
|
||||||
!define INFO_PRODUCTNAME "My Product"
|
!define INFO_PRODUCTNAME "gomuks desktop"
|
||||||
!endif
|
!endif
|
||||||
!ifndef INFO_PRODUCTVERSION
|
!ifndef INFO_PRODUCTVERSION
|
||||||
!define INFO_PRODUCTVERSION "0.1.0"
|
!define INFO_PRODUCTVERSION "0.4.0"
|
||||||
!endif
|
!endif
|
||||||
!ifndef INFO_COPYRIGHT
|
!ifndef INFO_COPYRIGHT
|
||||||
!define INFO_COPYRIGHT "© now, My Company"
|
!define INFO_COPYRIGHT "© 2024, gomuks authors"
|
||||||
!endif
|
!endif
|
||||||
!ifndef PRODUCT_EXECUTABLE
|
!ifndef PRODUCT_EXECUTABLE
|
||||||
!define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
|
!define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
|
||||||
|
@ -177,3 +177,36 @@ RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
|
||||||
SetDetailsPrint both
|
SetDetailsPrint both
|
||||||
ok:
|
ok:
|
||||||
!macroend
|
!macroend
|
||||||
|
|
||||||
|
# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b
|
||||||
|
!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND
|
||||||
|
; Backup the previously associated file class
|
||||||
|
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" ""
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0"
|
||||||
|
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}"
|
||||||
|
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}`
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}`
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open"
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}`
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}`
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro APP_UNASSOCIATE EXT FILECLASS
|
||||||
|
; Backup the previously associated file class
|
||||||
|
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup`
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0"
|
||||||
|
|
||||||
|
DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}`
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.associateFiles
|
||||||
|
; Create file associations
|
||||||
|
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.unassociateFiles
|
||||||
|
; Delete app associations
|
||||||
|
|
||||||
|
!macroend
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||||
<assemblyIdentity type="win32" name="com.wails.gomuks-desktop" version="0.1.0" processorArchitecture="*"/>
|
<assemblyIdentity type="win32" name="fi.mau.gomuks.desktop" version="0.4.0" processorArchitecture="*"/>
|
||||||
<dependency>
|
<dependency>
|
||||||
<dependentAssembly>
|
<dependentAssembly>
|
||||||
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
|
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
|
||||||
|
|
|
@ -4,37 +4,38 @@ go 1.23.0
|
||||||
|
|
||||||
toolchain go1.23.3
|
toolchain go1.23.3
|
||||||
|
|
||||||
require github.com/wailsapp/wails/v3 v3.0.0-alpha.7
|
require github.com/wailsapp/wails/v3 v3.0.0-alpha.8.3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
go.mau.fi/gomuks v0.3.1
|
go.mau.fi/gomuks v0.3.1
|
||||||
go.mau.fi/util v0.8.3-0.20241207221539-07bba6a0c5eb
|
go.mau.fi/util v0.8.4-0.20250106152331-30b8c95e7d7a
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
dario.cat/mergo v1.0.0 // indirect
|
dario.cat/mergo v1.0.1 // indirect
|
||||||
filippo.io/edwards25519 v1.1.0 // indirect
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect
|
github.com/ProtonMail/go-crypto v1.0.0 // indirect
|
||||||
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
|
github.com/adrg/xdg v0.5.0 // indirect
|
||||||
|
github.com/alecthomas/chroma/v2 v2.15.0 // indirect
|
||||||
github.com/bep/debounce v1.2.1 // indirect
|
github.com/bep/debounce v1.2.1 // indirect
|
||||||
github.com/buckket/go-blurhash v1.1.0 // indirect
|
github.com/buckket/go-blurhash v1.1.0 // indirect
|
||||||
github.com/chzyer/readline v1.5.1 // indirect
|
github.com/chzyer/readline v1.5.1 // indirect
|
||||||
github.com/cloudflare/circl v1.3.7 // indirect
|
github.com/cloudflare/circl v1.3.8 // indirect
|
||||||
github.com/coder/websocket v1.8.12 // indirect
|
github.com/coder/websocket v1.8.12 // indirect
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||||
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
|
github.com/cyphar/filepath-securejoin v0.2.5 // indirect
|
||||||
github.com/dlclark/regexp2 v1.11.0 // indirect
|
github.com/dlclark/regexp2 v1.11.4 // indirect
|
||||||
github.com/ebitengine/purego v0.4.0-alpha.4 // indirect
|
github.com/ebitengine/purego v0.4.0-alpha.4 // indirect
|
||||||
github.com/emirpasic/gods v1.18.1 // indirect
|
github.com/emirpasic/gods v1.18.1 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.7 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
github.com/go-git/go-billy/v5 v5.5.0 // indirect
|
github.com/go-git/go-billy/v5 v5.6.0 // indirect
|
||||||
github.com/go-git/go-git/v5 v5.11.0 // indirect
|
github.com/go-git/go-git/v5 v5.12.0 // indirect
|
||||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
github.com/google/uuid v1.3.0 // indirect
|
github.com/google/uuid v1.4.0 // indirect
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
||||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||||
|
@ -45,38 +46,38 @@ require (
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.24 // indirect
|
github.com/mattn/go-sqlite3 v1.14.24 // indirect
|
||||||
github.com/petermattis/goid v0.0.0-20241025130422-66cb2e6d7274 // indirect
|
github.com/petermattis/goid v0.0.0-20241211131331-93ee7e083c43 // indirect
|
||||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
|
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/rs/xid v1.6.0 // indirect
|
github.com/rs/xid v1.6.0 // indirect
|
||||||
github.com/rs/zerolog v1.33.0 // indirect
|
github.com/rs/zerolog v1.33.0 // indirect
|
||||||
github.com/samber/lo v1.38.1 // indirect
|
github.com/samber/lo v1.38.1 // indirect
|
||||||
github.com/sergi/go-diff v1.2.0 // indirect
|
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||||
github.com/skeema/knownhosts v1.2.1 // indirect
|
github.com/skeema/knownhosts v1.2.2 // indirect
|
||||||
github.com/tidwall/gjson v1.18.0 // indirect
|
github.com/tidwall/gjson v1.18.0 // indirect
|
||||||
github.com/tidwall/match v1.1.1 // indirect
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
github.com/tidwall/pretty v1.2.0 // indirect
|
github.com/tidwall/pretty v1.2.0 // indirect
|
||||||
github.com/tidwall/sjson v1.2.5 // indirect
|
github.com/tidwall/sjson v1.2.5 // indirect
|
||||||
github.com/wailsapp/go-webview2 v1.0.15 // indirect
|
github.com/wailsapp/go-webview2 v1.0.18 // indirect
|
||||||
github.com/wailsapp/mimetype v1.4.1 // indirect
|
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
github.com/yuin/goldmark v1.7.8 // indirect
|
github.com/yuin/goldmark v1.7.8 // indirect
|
||||||
go.mau.fi/zeroconfig v0.1.3 // indirect
|
go.mau.fi/zeroconfig v0.1.3 // indirect
|
||||||
golang.org/x/crypto v0.29.0 // indirect
|
golang.org/x/crypto v0.32.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect
|
golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 // indirect
|
||||||
golang.org/x/image v0.22.0 // indirect
|
golang.org/x/image v0.23.0 // indirect
|
||||||
golang.org/x/mod v0.22.0 // indirect
|
golang.org/x/mod v0.22.0 // indirect
|
||||||
golang.org/x/net v0.31.0 // indirect
|
golang.org/x/net v0.33.0 // indirect
|
||||||
golang.org/x/sync v0.9.0 // indirect
|
golang.org/x/sync v0.10.0 // indirect
|
||||||
golang.org/x/sys v0.27.0 // indirect
|
golang.org/x/sys v0.29.0 // indirect
|
||||||
golang.org/x/text v0.20.0 // indirect
|
golang.org/x/text v0.21.0 // indirect
|
||||||
golang.org/x/tools v0.27.0 // indirect
|
golang.org/x/tools v0.28.0 // indirect
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
maunium.net/go/mautrix v0.22.1-0.20241207130433-421bd5c4c837 // indirect
|
maunium.net/go/mautrix v0.22.2-0.20250106152426-68eaa9d1df1f // indirect
|
||||||
mvdan.cc/xurls/v2 v2.5.0 // indirect
|
mvdan.cc/xurls/v2 v2.6.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
replace go.mau.fi/gomuks => ../
|
replace go.mau.fi/gomuks => ../
|
||||||
|
|
138
desktop/go.sum
|
@ -1,5 +1,5 @@
|
||||||
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||||
|
@ -7,12 +7,14 @@ github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ
|
||||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||||
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
||||||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg=
|
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
||||||
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
|
github.com/adrg/xdg v0.5.0 h1:dDaZvhMXatArP1NPHhnfaQUqWBLBsmx1h1HXQdMoFCY=
|
||||||
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
github.com/adrg/xdg v0.5.0/go.mod h1:dDdY4M4DF9Rjy4kHPeNL+ilVF+p2lK8IdM9/rTSGcI4=
|
||||||
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
|
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||||
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
|
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||||
|
github.com/alecthomas/chroma/v2 v2.15.0 h1:LxXTQHFoYrstG2nnV9y2X5O94sOBzf0CIUpSTbpxvMc=
|
||||||
|
github.com/alecthomas/chroma/v2 v2.15.0/go.mod h1:gUhVLrPDXPtp/f+L1jo9xepo9gL4eLwRuGAunSZMkio=
|
||||||
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||||
|
@ -31,39 +33,39 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk
|
||||||
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
|
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
|
||||||
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
||||||
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
||||||
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
|
github.com/cloudflare/circl v1.3.8 h1:j+V8jJt09PoeMFIu2uh5JUyEaIHTXVOHslFoLNAKqwI=
|
||||||
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
|
github.com/cloudflare/circl v1.3.8/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
|
||||||
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
||||||
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
|
github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo=
|
||||||
github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
|
github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
||||||
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/ebitengine/purego v0.4.0-alpha.4 h1:Y7yIV06Yo5M2BAdD7EVPhfp6LZ0tEcQo5770OhYUVes=
|
github.com/ebitengine/purego v0.4.0-alpha.4 h1:Y7yIV06Yo5M2BAdD7EVPhfp6LZ0tEcQo5770OhYUVes=
|
||||||
github.com/ebitengine/purego v0.4.0-alpha.4/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
|
github.com/ebitengine/purego v0.4.0-alpha.4/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
|
||||||
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
|
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
|
||||||
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
|
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
|
||||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA=
|
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU=
|
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||||
github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
|
github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
|
||||||
github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4=
|
github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8=
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||||
github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
|
github.com/go-git/go-billy/v5 v5.6.0 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8=
|
||||||
github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
|
github.com/go-git/go-billy/v5 v5.6.0/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM=
|
||||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||||
github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4=
|
github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
|
||||||
github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY=
|
github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY=
|
||||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
@ -71,8 +73,8 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||||
|
@ -106,10 +108,10 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||||
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
|
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
||||||
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
|
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
||||||
github.com/petermattis/goid v0.0.0-20241025130422-66cb2e6d7274 h1:qli3BGQK0tYDkSEvZ/FzZTi9ZrOX86Q6CIhKLGc489A=
|
github.com/petermattis/goid v0.0.0-20241211131331-93ee7e083c43 h1:ah1dvbqPMN5+ocrg/ZSgZ6k8bOk+kcZQ7fnyx6UvOm4=
|
||||||
github.com/petermattis/goid v0.0.0-20241025130422-66cb2e6d7274/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
github.com/petermattis/goid v0.0.0-20241211131331-93ee7e083c43/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
||||||
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
|
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
|
||||||
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
|
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
|
||||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
|
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
|
||||||
|
@ -121,8 +123,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a h1:w3tdWGKbLGBPtR/8/oO74W6hmz0qE5q0z9aqSAewaaM=
|
||||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a/go.mod h1:S8kfXMp+yh77OxPD4fdM6YUknrZpQxLhvxzS4gDHENY=
|
||||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
|
@ -130,16 +132,16 @@ github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||||
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||||
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
|
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
|
||||||
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
|
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
|
||||||
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
|
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
||||||
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||||
github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ=
|
github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A=
|
||||||
github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
|
github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
@ -149,19 +151,19 @@ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||||
github.com/wailsapp/go-webview2 v1.0.15 h1:IeQFoWmsHp32y64I41J+Zod3SopjHs918KSO4jUqEnY=
|
github.com/wailsapp/go-webview2 v1.0.18 h1:SSSCoLA+MYikSp1U0WmvELF/4c3x5kH8Vi31TKyZ4yk=
|
||||||
github.com/wailsapp/go-webview2 v1.0.15/go.mod h1:Uk2BePfCRzttBBjFrBmqKGJd41P6QIHeV9kTgIeOZNo=
|
github.com/wailsapp/go-webview2 v1.0.18/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
||||||
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
||||||
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
||||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.7 h1:LNX2EnbxTEYJYICJT8UkuzoGVNalRizTNGBY47endmk=
|
github.com/wailsapp/wails/v3 v3.0.0-alpha.8.3 h1:9aCL0IXD60A5iscQ/ps6f3ti3IlaoG6LQe0RZ9JkueU=
|
||||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.7/go.mod h1:lBz4zedFxreJBoVpMe9u89oo4IE3IlyHJg5rOWnGNR0=
|
github.com/wailsapp/wails/v3 v3.0.0-alpha.8.3/go.mod h1:9Ca1goy5oqxmy8Oetb8Tchkezcx4tK03DK+SqYByu5Y=
|
||||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
||||||
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||||
go.mau.fi/util v0.8.3-0.20241207221539-07bba6a0c5eb h1:/iKi+4aRvd8LZJ3z1UQjxmFdDVfJuDWClc/4MToWnSY=
|
go.mau.fi/util v0.8.4-0.20250106152331-30b8c95e7d7a h1:D9RCHBFjxah9F/YB7amvRJjT2IEOFWcz8jpcEY8dBV0=
|
||||||
go.mau.fi/util v0.8.3-0.20241207221539-07bba6a0c5eb/go.mod h1:BHHC9R2WLMJd1bwTZfTcFxUgRFmUgUmiWcT4RbzUgiA=
|
go.mau.fi/util v0.8.4-0.20250106152331-30b8c95e7d7a/go.mod h1:MOfGTs1CBuK6ERTcSL4lb5YU7/ujz09eOPVEDckuazY=
|
||||||
go.mau.fi/zeroconfig v0.1.3 h1:As9wYDKmktjmNZW5i1vn8zvJlmGKHeVxHVIBMXsm4kM=
|
go.mau.fi/zeroconfig v0.1.3 h1:As9wYDKmktjmNZW5i1vn8zvJlmGKHeVxHVIBMXsm4kM=
|
||||||
go.mau.fi/zeroconfig v0.1.3/go.mod h1:NcSJkf180JT+1IId76PcMuLTNa1CzsFFZ0nBygIQM70=
|
go.mau.fi/zeroconfig v0.1.3/go.mod h1:NcSJkf180JT+1IId76PcMuLTNa1CzsFFZ0nBygIQM70=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
@ -169,12 +171,12 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
|
||||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||||
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||||
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||||
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo=
|
golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 h1:9kj3STMvgqy3YA4VQXBrN7925ICMxD5wzMRcgA30588=
|
||||||
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak=
|
golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
|
||||||
golang.org/x/image v0.22.0 h1:UtK5yLUzilVrkjMAZAZ34DXGpASN8i8pj8g+O+yd10g=
|
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
|
||||||
golang.org/x/image v0.22.0/go.mod h1:9hPFhljd4zZ1GNSIZJ49sqbp45GKK9t6w+iXvGqZUz4=
|
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
||||||
|
@ -187,15 +189,14 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
||||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||||
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||||
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
|
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||||
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
@ -208,20 +209,21 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||||
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
|
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||||
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
|
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
@ -229,14 +231,14 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o=
|
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
|
||||||
golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q=
|
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
@ -247,10 +249,10 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYs
|
||||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
maunium.net/go/mautrix v0.22.1-0.20241207130433-421bd5c4c837 h1:v3cRnMfhKxpnKjhikZ5HY72MKIsgYzldL2s3cqbkNbY=
|
maunium.net/go/mautrix v0.22.2-0.20250106152426-68eaa9d1df1f h1:+nAznNCSCm+P4am9CmguSfNfcbpA7qtTzJQrwCP8aHM=
|
||||||
maunium.net/go/mautrix v0.22.1-0.20241207130433-421bd5c4c837/go.mod h1:oqwf9WYC/brqucM+heYk4gX11O59nP+ljvyxVhndFIM=
|
maunium.net/go/mautrix v0.22.2-0.20250106152426-68eaa9d1df1f/go.mod h1:FmwzK7RSzrd1OfGDgJzFWXl7nYmYm8/P0Y77sy/A1Uw=
|
||||||
mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8=
|
mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI=
|
||||||
mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE=
|
mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk=
|
||||||
|
|
|
@ -123,7 +123,7 @@ func main() {
|
||||||
ch := &CommandHandler{Gomuks: gmx, Ctx: cmdCtx}
|
ch := &CommandHandler{Gomuks: gmx, Ctx: cmdCtx}
|
||||||
app := application.New(application.Options{
|
app := application.New(application.Options{
|
||||||
Name: "gomuks-desktop",
|
Name: "gomuks-desktop",
|
||||||
Description: "A Matrix client written in Go",
|
Description: "A Matrix client written in Go and React",
|
||||||
Services: []application.Service{
|
Services: []application.Service{
|
||||||
application.NewService(
|
application.NewService(
|
||||||
&PointableHandler{gmx.CreateAPIRouter()},
|
&PointableHandler{gmx.CreateAPIRouter()},
|
||||||
|
|
30
go.mod
|
@ -2,14 +2,14 @@ module go.mau.fi/gomuks
|
||||||
|
|
||||||
go 1.23.0
|
go 1.23.0
|
||||||
|
|
||||||
toolchain go1.23.3
|
toolchain go1.23.4
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/alecthomas/chroma/v2 v2.14.0
|
github.com/alecthomas/chroma/v2 v2.15.0
|
||||||
github.com/buckket/go-blurhash v1.1.0
|
github.com/buckket/go-blurhash v1.1.0
|
||||||
github.com/chzyer/readline v1.5.1
|
github.com/chzyer/readline v1.5.1
|
||||||
github.com/coder/websocket v1.8.12
|
github.com/coder/websocket v1.8.12
|
||||||
github.com/gabriel-vasile/mimetype v1.4.7
|
github.com/gabriel-vasile/mimetype v1.4.8
|
||||||
github.com/gdamore/tcell/v2 v2.7.4
|
github.com/gdamore/tcell/v2 v2.7.4
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0
|
github.com/lucasb-eyer/go-colorful v1.2.0
|
||||||
github.com/mattn/go-sqlite3 v1.14.24
|
github.com/mattn/go-sqlite3 v1.14.24
|
||||||
|
@ -19,33 +19,33 @@ require (
|
||||||
github.com/tidwall/sjson v1.2.5
|
github.com/tidwall/sjson v1.2.5
|
||||||
github.com/yuin/goldmark v1.7.8
|
github.com/yuin/goldmark v1.7.8
|
||||||
go.mau.fi/mauview v0.2.2-0.20241209125653-292e0a6914c5
|
go.mau.fi/mauview v0.2.2-0.20241209125653-292e0a6914c5
|
||||||
go.mau.fi/util v0.8.3-0.20241207221539-07bba6a0c5eb
|
go.mau.fi/util v0.8.4-0.20250106152331-30b8c95e7d7a
|
||||||
go.mau.fi/zeroconfig v0.1.3
|
go.mau.fi/zeroconfig v0.1.3
|
||||||
golang.org/x/crypto v0.29.0
|
golang.org/x/crypto v0.32.0
|
||||||
golang.org/x/image v0.22.0
|
golang.org/x/image v0.23.0
|
||||||
golang.org/x/net v0.31.0
|
golang.org/x/net v0.33.0
|
||||||
golang.org/x/text v0.20.0
|
golang.org/x/text v0.21.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
maunium.net/go/mauflag v1.0.0
|
maunium.net/go/mauflag v1.0.0
|
||||||
maunium.net/go/mautrix v0.22.1-0.20241207130433-421bd5c4c837
|
maunium.net/go/mautrix v0.22.2-0.20250106152426-68eaa9d1df1f
|
||||||
mvdan.cc/xurls/v2 v2.5.0
|
mvdan.cc/xurls/v2 v2.6.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
filippo.io/edwards25519 v1.1.0 // indirect
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||||
github.com/dlclark/regexp2 v1.11.0 // indirect
|
github.com/dlclark/regexp2 v1.11.4 // indirect
|
||||||
github.com/gdamore/encoding v1.0.0 // indirect
|
github.com/gdamore/encoding v1.0.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
github.com/petermattis/goid v0.0.0-20241025130422-66cb2e6d7274 // indirect
|
github.com/petermattis/goid v0.0.0-20241211131331-93ee7e083c43 // indirect
|
||||||
github.com/rs/xid v1.6.0 // indirect
|
github.com/rs/xid v1.6.0 // indirect
|
||||||
github.com/tidwall/match v1.1.1 // indirect
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
github.com/tidwall/pretty v1.2.0 // indirect
|
github.com/tidwall/pretty v1.2.0 // indirect
|
||||||
github.com/zyedidia/clipboard v1.0.4 // indirect
|
github.com/zyedidia/clipboard v1.0.4 // indirect
|
||||||
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect
|
golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 // indirect
|
||||||
golang.org/x/sys v0.27.0 // indirect
|
golang.org/x/sys v0.29.0 // indirect
|
||||||
golang.org/x/term v0.26.0 // indirect
|
golang.org/x/term v0.28.0 // indirect
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
64
go.sum
|
@ -2,10 +2,10 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||||
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
|
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||||
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||||
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
|
github.com/alecthomas/chroma/v2 v2.15.0 h1:LxXTQHFoYrstG2nnV9y2X5O94sOBzf0CIUpSTbpxvMc=
|
||||||
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
|
github.com/alecthomas/chroma/v2 v2.15.0/go.mod h1:gUhVLrPDXPtp/f+L1jo9xepo9gL4eLwRuGAunSZMkio=
|
||||||
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||||
github.com/buckket/go-blurhash v1.1.0 h1:X5M6r0LIvwdvKiUtiNcRL2YlmOfMzYobI3VCKCZc9Do=
|
github.com/buckket/go-blurhash v1.1.0 h1:X5M6r0LIvwdvKiUtiNcRL2YlmOfMzYobI3VCKCZc9Do=
|
||||||
|
@ -22,10 +22,10 @@ github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
||||||
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA=
|
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU=
|
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||||
github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU=
|
github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU=
|
||||||
|
@ -45,8 +45,8 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T
|
||||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||||
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/petermattis/goid v0.0.0-20241025130422-66cb2e6d7274 h1:qli3BGQK0tYDkSEvZ/FzZTi9ZrOX86Q6CIhKLGc489A=
|
github.com/petermattis/goid v0.0.0-20241211131331-93ee7e083c43 h1:ah1dvbqPMN5+ocrg/ZSgZ6k8bOk+kcZQ7fnyx6UvOm4=
|
||||||
github.com/petermattis/goid v0.0.0-20241025130422-66cb2e6d7274/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
github.com/petermattis/goid v0.0.0-20241211131331-93ee7e083c43/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
@ -59,8 +59,8 @@ github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||||
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
@ -77,26 +77,26 @@ github.com/zyedidia/clipboard v1.0.4 h1:r6GUQOyPtIaApRLeD56/U+2uJbXis6ANGbKWCljU
|
||||||
github.com/zyedidia/clipboard v1.0.4/go.mod h1:zykFnZUXX0ErxqvYLUFEq7QDJKId8rmh2FgD0/Y8cjA=
|
github.com/zyedidia/clipboard v1.0.4/go.mod h1:zykFnZUXX0ErxqvYLUFEq7QDJKId8rmh2FgD0/Y8cjA=
|
||||||
go.mau.fi/mauview v0.2.2-0.20241209125653-292e0a6914c5 h1:apKftqeRRyj/Vpd5s81fNhS8UErwgfs07KG3NSHB/4Q=
|
go.mau.fi/mauview v0.2.2-0.20241209125653-292e0a6914c5 h1:apKftqeRRyj/Vpd5s81fNhS8UErwgfs07KG3NSHB/4Q=
|
||||||
go.mau.fi/mauview v0.2.2-0.20241209125653-292e0a6914c5/go.mod h1:G0Qkfwt84f+5tagHsaRdiTuUFeTlIZu61MN/JL9D8Qo=
|
go.mau.fi/mauview v0.2.2-0.20241209125653-292e0a6914c5/go.mod h1:G0Qkfwt84f+5tagHsaRdiTuUFeTlIZu61MN/JL9D8Qo=
|
||||||
go.mau.fi/util v0.8.3-0.20241207221539-07bba6a0c5eb h1:/iKi+4aRvd8LZJ3z1UQjxmFdDVfJuDWClc/4MToWnSY=
|
go.mau.fi/util v0.8.4-0.20250106152331-30b8c95e7d7a h1:D9RCHBFjxah9F/YB7amvRJjT2IEOFWcz8jpcEY8dBV0=
|
||||||
go.mau.fi/util v0.8.3-0.20241207221539-07bba6a0c5eb/go.mod h1:BHHC9R2WLMJd1bwTZfTcFxUgRFmUgUmiWcT4RbzUgiA=
|
go.mau.fi/util v0.8.4-0.20250106152331-30b8c95e7d7a/go.mod h1:MOfGTs1CBuK6ERTcSL4lb5YU7/ujz09eOPVEDckuazY=
|
||||||
go.mau.fi/zeroconfig v0.1.3 h1:As9wYDKmktjmNZW5i1vn8zvJlmGKHeVxHVIBMXsm4kM=
|
go.mau.fi/zeroconfig v0.1.3 h1:As9wYDKmktjmNZW5i1vn8zvJlmGKHeVxHVIBMXsm4kM=
|
||||||
go.mau.fi/zeroconfig v0.1.3/go.mod h1:NcSJkf180JT+1IId76PcMuLTNa1CzsFFZ0nBygIQM70=
|
go.mau.fi/zeroconfig v0.1.3/go.mod h1:NcSJkf180JT+1IId76PcMuLTNa1CzsFFZ0nBygIQM70=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||||
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||||
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo=
|
golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 h1:9kj3STMvgqy3YA4VQXBrN7925ICMxD5wzMRcgA30588=
|
||||||
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak=
|
golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
|
||||||
golang.org/x/image v0.22.0 h1:UtK5yLUzilVrkjMAZAZ34DXGpASN8i8pj8g+O+yd10g=
|
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
|
||||||
golang.org/x/image v0.22.0/go.mod h1:9hPFhljd4zZ1GNSIZJ49sqbp45GKK9t6w+iXvGqZUz4=
|
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||||
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
@ -111,21 +111,21 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
|
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||||
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
|
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
@ -139,7 +139,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
|
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
|
||||||
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
|
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
|
||||||
maunium.net/go/mautrix v0.22.1-0.20241207130433-421bd5c4c837 h1:v3cRnMfhKxpnKjhikZ5HY72MKIsgYzldL2s3cqbkNbY=
|
maunium.net/go/mautrix v0.22.2-0.20250106152426-68eaa9d1df1f h1:+nAznNCSCm+P4am9CmguSfNfcbpA7qtTzJQrwCP8aHM=
|
||||||
maunium.net/go/mautrix v0.22.1-0.20241207130433-421bd5c4c837/go.mod h1:oqwf9WYC/brqucM+heYk4gX11O59nP+ljvyxVhndFIM=
|
maunium.net/go/mautrix v0.22.2-0.20250106152426-68eaa9d1df1f/go.mod h1:FmwzK7RSzrd1OfGDgJzFWXl7nYmYm8/P0Y77sy/A1Uw=
|
||||||
mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8=
|
mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI=
|
||||||
mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE=
|
mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk=
|
||||||
|
|
|
@ -54,7 +54,7 @@ func NewEventBuffer(maxSize int) *EventBuffer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (eb *EventBuffer) HicliEventHandler(evt any) {
|
func (eb *EventBuffer) Push(evt any) {
|
||||||
data, err := json.Marshal(evt)
|
data, err := json.Marshal(evt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Errorf("failed to marshal event %T: %w", evt, err))
|
panic(fmt.Errorf("failed to marshal event %T: %w", evt, err))
|
||||||
|
|
|
@ -34,6 +34,7 @@ import (
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Web WebConfig `yaml:"web"`
|
Web WebConfig `yaml:"web"`
|
||||||
Matrix MatrixConfig `yaml:"matrix"`
|
Matrix MatrixConfig `yaml:"matrix"`
|
||||||
|
Push PushConfig `yaml:"push"`
|
||||||
Logging zeroconfig.Config `yaml:"logging"`
|
Logging zeroconfig.Config `yaml:"logging"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,13 +42,18 @@ type MatrixConfig struct {
|
||||||
DisableHTTP2 bool `yaml:"disable_http2"`
|
DisableHTTP2 bool `yaml:"disable_http2"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PushConfig struct {
|
||||||
|
FCMGateway string `yaml:"fcm_gateway"`
|
||||||
|
}
|
||||||
|
|
||||||
type WebConfig struct {
|
type WebConfig struct {
|
||||||
ListenAddress string `yaml:"listen_address"`
|
ListenAddress string `yaml:"listen_address"`
|
||||||
Username string `yaml:"username"`
|
Username string `yaml:"username"`
|
||||||
PasswordHash string `yaml:"password_hash"`
|
PasswordHash string `yaml:"password_hash"`
|
||||||
TokenKey string `yaml:"token_key"`
|
TokenKey string `yaml:"token_key"`
|
||||||
DebugEndpoints bool `yaml:"debug_endpoints"`
|
DebugEndpoints bool `yaml:"debug_endpoints"`
|
||||||
EventBufferSize int `yaml:"event_buffer_size"`
|
EventBufferSize int `yaml:"event_buffer_size"`
|
||||||
|
OriginPatterns []string `yaml:"origin_patterns"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultFileWriter = zeroconfig.WriterConfig{
|
var defaultFileWriter = zeroconfig.WriterConfig{
|
||||||
|
@ -120,6 +126,14 @@ func (gmx *Gomuks) LoadConfig() error {
|
||||||
gmx.Config.Web.EventBufferSize = 512
|
gmx.Config.Web.EventBufferSize = 512
|
||||||
changed = true
|
changed = true
|
||||||
}
|
}
|
||||||
|
if gmx.Config.Push.FCMGateway == "" {
|
||||||
|
gmx.Config.Push.FCMGateway = "https://push.gomuks.app"
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if len(gmx.Config.Web.OriginPatterns) == 0 {
|
||||||
|
gmx.Config.Web.OriginPatterns = []string{"localhost:*", "*.localhost:*"}
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
if changed {
|
if changed {
|
||||||
err = gmx.SaveConfig()
|
err = gmx.SaveConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -35,6 +35,7 @@ import (
|
||||||
"go.mau.fi/util/dbutil"
|
"go.mau.fi/util/dbutil"
|
||||||
"go.mau.fi/util/exerrors"
|
"go.mau.fi/util/exerrors"
|
||||||
"go.mau.fi/util/exzerolog"
|
"go.mau.fi/util/exzerolog"
|
||||||
|
"go.mau.fi/util/ptr"
|
||||||
"go.mau.fi/zeroconfig"
|
"go.mau.fi/zeroconfig"
|
||||||
"golang.org/x/net/http2"
|
"golang.org/x/net/http2"
|
||||||
|
|
||||||
|
@ -184,7 +185,7 @@ func (gmx *Gomuks) StartClient() {
|
||||||
nil,
|
nil,
|
||||||
gmx.Log.With().Str("component", "hicli").Logger(),
|
gmx.Log.With().Str("component", "hicli").Logger(),
|
||||||
[]byte("meow"),
|
[]byte("meow"),
|
||||||
gmx.EventBuffer.HicliEventHandler,
|
gmx.HandleEvent,
|
||||||
)
|
)
|
||||||
gmx.Client.LogoutFunc = gmx.Logout
|
gmx.Client.LogoutFunc = gmx.Logout
|
||||||
httpClient := gmx.Client.Client.Client
|
httpClient := gmx.Client.Client.Client
|
||||||
|
@ -210,6 +211,14 @@ func (gmx *Gomuks) StartClient() {
|
||||||
gmx.Log.Info().Stringer("user_id", userID).Msg("Client started")
|
gmx.Log.Info().Stringer("user_id", userID).Msg("Client started")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (gmx *Gomuks) HandleEvent(evt any) {
|
||||||
|
gmx.EventBuffer.Push(evt)
|
||||||
|
syncComplete, ok := evt.(*hicli.SyncComplete)
|
||||||
|
if ok && ptr.Val(syncComplete.Since) != "" {
|
||||||
|
go gmx.SendPushNotifications(syncComplete)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (gmx *Gomuks) Stop() {
|
func (gmx *Gomuks) Stop() {
|
||||||
gmx.stopOnce.Do(func() {
|
gmx.stopOnce.Do(func() {
|
||||||
close(gmx.stopChan)
|
close(gmx.stopChan)
|
||||||
|
@ -230,9 +239,11 @@ func (gmx *Gomuks) DirectStop() {
|
||||||
closer(websocket.StatusServiceRestart, "Server shutting down")
|
closer(websocket.StatusServiceRestart, "Server shutting down")
|
||||||
}
|
}
|
||||||
gmx.Client.Stop()
|
gmx.Client.Stop()
|
||||||
err := gmx.Server.Close()
|
if gmx.Server != nil {
|
||||||
if err != nil {
|
err := gmx.Server.Close()
|
||||||
gmx.Log.Error().Err(err).Msg("Failed to close server")
|
if err != nil {
|
||||||
|
gmx.Log.Error().Err(err).Msg("Failed to close server")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -109,7 +109,7 @@ func cacheEntryToHeaders(w http.ResponseWriter, entry *database.Media) {
|
||||||
w.Header().Set("Content-Type", entry.MimeType)
|
w.Header().Set("Content-Type", entry.MimeType)
|
||||||
w.Header().Set("Content-Length", strconv.FormatInt(entry.Size, 10))
|
w.Header().Set("Content-Length", strconv.FormatInt(entry.Size, 10))
|
||||||
w.Header().Set("Content-Disposition", mime.FormatMediaType(entry.ContentDisposition(), map[string]string{"filename": entry.FileName}))
|
w.Header().Set("Content-Disposition", mime.FormatMediaType(entry.ContentDisposition(), map[string]string{"filename": entry.FileName}))
|
||||||
w.Header().Set("Content-Security-Policy", "sandbox; default-src 'none'; script-src 'none';")
|
w.Header().Set("Content-Security-Policy", "sandbox; default-src 'none'; script-src 'none'; media-src 'self';")
|
||||||
w.Header().Set("Cache-Control", "max-age=2592000, immutable")
|
w.Header().Set("Cache-Control", "max-age=2592000, immutable")
|
||||||
w.Header().Set("ETag", entry.ETag())
|
w.Header().Set("ETag", entry.ETag())
|
||||||
}
|
}
|
||||||
|
@ -125,7 +125,7 @@ func (new *noErrorWriter) Write(p []byte) (n int, err error) {
|
||||||
|
|
||||||
// note: this should stay in sync with makeAvatarFallback in web/src/api/media.ts
|
// note: this should stay in sync with makeAvatarFallback in web/src/api/media.ts
|
||||||
const fallbackAvatarTemplate = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000">
|
const fallbackAvatarTemplate = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000">
|
||||||
<circle cx="500" cy="500" r="500" fill="%s"/>
|
<rect x="0" y="0" width="1000" height="1000" fill="%s"/>
|
||||||
<text x="500" y="750" text-anchor="middle" fill="#fff" font-weight="bold" font-size="666"
|
<text x="500" y="750" text-anchor="middle" fill="#fff" font-weight="bold" font-size="666"
|
||||||
font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif"
|
font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif"
|
||||||
>%s</text>
|
>%s</text>
|
||||||
|
|
252
pkg/gomuks/push.go
Normal file
|
@ -0,0 +1,252 @@
|
||||||
|
// gomuks - A Matrix client written in Go.
|
||||||
|
// Copyright (C) 2025 Tulir Asokan
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package gomuks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"go.mau.fi/util/jsontime"
|
||||||
|
"go.mau.fi/util/ptr"
|
||||||
|
"go.mau.fi/util/random"
|
||||||
|
"maunium.net/go/mautrix/id"
|
||||||
|
|
||||||
|
"go.mau.fi/gomuks/pkg/hicli"
|
||||||
|
"go.mau.fi/gomuks/pkg/hicli/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PushNotification struct {
|
||||||
|
Dismiss []PushDismiss `json:"dismiss,omitempty"`
|
||||||
|
OrigMessages []*PushNewMessage `json:"-"`
|
||||||
|
RawMessages []json.RawMessage `json:"messages,omitempty"`
|
||||||
|
ImageAuth string `json:"image_auth,omitempty"`
|
||||||
|
ImageAuthExpiry *jsontime.UnixMilli `json:"image_auth_expiry,omitempty"`
|
||||||
|
HasImportant bool `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PushDismiss struct {
|
||||||
|
RoomID id.RoomID `json:"room_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var pushClient = &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
DialContext: (&net.Dialer{Timeout: 10 * time.Second}).DialContext,
|
||||||
|
ResponseHeaderTimeout: 10 * time.Second,
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
ForceAttemptHTTP2: true,
|
||||||
|
MaxIdleConns: 5,
|
||||||
|
IdleConnTimeout: 90 * time.Second,
|
||||||
|
TLSHandshakeTimeout: 10 * time.Second,
|
||||||
|
ExpectContinueTimeout: 1 * time.Second,
|
||||||
|
},
|
||||||
|
Timeout: 60 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gmx *Gomuks) SendPushNotifications(sync *hicli.SyncComplete) {
|
||||||
|
var ctx context.Context
|
||||||
|
var push PushNotification
|
||||||
|
for _, room := range sync.Rooms {
|
||||||
|
if room.DismissNotifications && len(push.Dismiss) < 10 {
|
||||||
|
push.Dismiss = append(push.Dismiss, PushDismiss{RoomID: room.Meta.ID})
|
||||||
|
}
|
||||||
|
for _, notif := range room.Notifications {
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = gmx.Log.With().
|
||||||
|
Str("action", "send push notification").
|
||||||
|
Logger().WithContext(context.Background())
|
||||||
|
}
|
||||||
|
msg := gmx.formatPushNotificationMessage(ctx, notif)
|
||||||
|
if msg == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
msgJSON, err := json.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
zerolog.Ctx(ctx).Err(err).
|
||||||
|
Int64("event_rowid", int64(notif.RowID)).
|
||||||
|
Stringer("event_id", notif.Event.ID).
|
||||||
|
Msg("Failed to marshal push notification")
|
||||||
|
continue
|
||||||
|
} else if len(msgJSON) > 1500 {
|
||||||
|
// This should not happen as long as formatPushNotificationMessage doesn't return too long messages
|
||||||
|
zerolog.Ctx(ctx).Error().
|
||||||
|
Int64("event_rowid", int64(notif.RowID)).
|
||||||
|
Stringer("event_id", notif.Event.ID).
|
||||||
|
Msg("Push notification too long")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
push.RawMessages = append(push.RawMessages, msgJSON)
|
||||||
|
push.OrigMessages = append(push.OrigMessages, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(push.Dismiss) == 0 && len(push.RawMessages) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = gmx.Log.With().
|
||||||
|
Str("action", "send push notification").
|
||||||
|
Logger().WithContext(context.Background())
|
||||||
|
}
|
||||||
|
pushRegs, err := gmx.Client.DB.PushRegistration.GetAll(ctx)
|
||||||
|
if err != nil {
|
||||||
|
zerolog.Ctx(ctx).Err(err).Msg("Failed to get push registrations")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(push.RawMessages) > 0 {
|
||||||
|
exp := time.Now().Add(24 * time.Hour)
|
||||||
|
push.ImageAuth = gmx.generateImageToken(24 * time.Hour)
|
||||||
|
push.ImageAuthExpiry = ptr.Ptr(jsontime.UM(exp))
|
||||||
|
}
|
||||||
|
for notif := range push.Split {
|
||||||
|
gmx.SendPushNotification(ctx, pushRegs, notif)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pn *PushNotification) Split(yield func(*PushNotification) bool) {
|
||||||
|
const maxSize = 2000
|
||||||
|
currentSize := 0
|
||||||
|
offset := 0
|
||||||
|
hasSound := false
|
||||||
|
for i, msg := range pn.RawMessages {
|
||||||
|
if len(msg) >= maxSize {
|
||||||
|
// This is already checked in SendPushNotifications, so this should never happen
|
||||||
|
panic("push notification message too long")
|
||||||
|
}
|
||||||
|
if currentSize+len(msg) > maxSize {
|
||||||
|
yield(&PushNotification{
|
||||||
|
Dismiss: pn.Dismiss,
|
||||||
|
RawMessages: pn.RawMessages[offset:i],
|
||||||
|
ImageAuth: pn.ImageAuth,
|
||||||
|
HasImportant: hasSound,
|
||||||
|
})
|
||||||
|
offset = i
|
||||||
|
currentSize = 0
|
||||||
|
hasSound = false
|
||||||
|
}
|
||||||
|
currentSize += len(msg)
|
||||||
|
hasSound = hasSound || pn.OrigMessages[i].Sound
|
||||||
|
}
|
||||||
|
yield(&PushNotification{
|
||||||
|
Dismiss: pn.Dismiss,
|
||||||
|
RawMessages: pn.RawMessages[offset:],
|
||||||
|
ImageAuth: pn.ImageAuth,
|
||||||
|
HasImportant: hasSound,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gmx *Gomuks) SendPushNotification(ctx context.Context, pushRegs []*database.PushRegistration, notif *PushNotification) {
|
||||||
|
rawPayload, err := json.Marshal(notif)
|
||||||
|
if err != nil {
|
||||||
|
zerolog.Ctx(ctx).Err(err).Msg("Failed to marshal push notification")
|
||||||
|
return
|
||||||
|
} else if base64.StdEncoding.EncodedLen(len(rawPayload)) >= 4000 {
|
||||||
|
zerolog.Ctx(ctx).Error().Msg("Generated push payload too long")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, reg := range pushRegs {
|
||||||
|
devicePayload := rawPayload
|
||||||
|
encrypted := false
|
||||||
|
if reg.Encryption.Key != nil {
|
||||||
|
var err error
|
||||||
|
devicePayload, err = encryptPush(rawPayload, reg.Encryption.Key)
|
||||||
|
if err != nil {
|
||||||
|
zerolog.Ctx(ctx).Err(err).Str("device_id", reg.DeviceID).Msg("Failed to encrypt push payload")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
encrypted = true
|
||||||
|
}
|
||||||
|
switch reg.Type {
|
||||||
|
case database.PushTypeFCM:
|
||||||
|
if !encrypted {
|
||||||
|
zerolog.Ctx(ctx).Warn().
|
||||||
|
Str("device_id", reg.DeviceID).
|
||||||
|
Msg("FCM push registration doesn't have encryption key")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var token string
|
||||||
|
err = json.Unmarshal(reg.Data, &token)
|
||||||
|
if err != nil {
|
||||||
|
zerolog.Ctx(ctx).Err(err).Str("device_id", reg.DeviceID).Msg("Failed to unmarshal FCM token")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
gmx.SendFCMPush(ctx, token, devicePayload, notif.HasImportant)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func encryptPush(payload, key []byte) ([]byte, error) {
|
||||||
|
if len(key) != 32 {
|
||||||
|
return nil, fmt.Errorf("encryption key must be 32 bytes long")
|
||||||
|
}
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create AES cipher: %w", err)
|
||||||
|
}
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create GCM cipher: %w", err)
|
||||||
|
}
|
||||||
|
iv := random.Bytes(12)
|
||||||
|
encrypted := make([]byte, 12, 12+len(payload))
|
||||||
|
copy(encrypted, iv)
|
||||||
|
return gcm.Seal(encrypted, iv, payload, nil), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type PushRequest struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
Payload []byte `json:"payload"`
|
||||||
|
HighPriority bool `json:"high_priority"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gmx *Gomuks) SendFCMPush(ctx context.Context, token string, payload []byte, highPriority bool) {
|
||||||
|
wrappedPayload, _ := json.Marshal(&PushRequest{
|
||||||
|
Token: token,
|
||||||
|
Payload: payload,
|
||||||
|
HighPriority: highPriority,
|
||||||
|
})
|
||||||
|
url := fmt.Sprintf("%s/_gomuks/push/fcm", gmx.Config.Push.FCMGateway)
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(wrappedPayload))
|
||||||
|
if err != nil {
|
||||||
|
zerolog.Ctx(ctx).Err(err).Msg("Failed to create push request")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := pushClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
zerolog.Ctx(ctx).Err(err).Str("push_token", token).Msg("Failed to send push request")
|
||||||
|
} else if resp.StatusCode != http.StatusOK {
|
||||||
|
zerolog.Ctx(ctx).Error().
|
||||||
|
Int("status", resp.StatusCode).
|
||||||
|
Str("push_token", token).
|
||||||
|
Msg("Non-200 status while sending push request")
|
||||||
|
} else {
|
||||||
|
zerolog.Ctx(ctx).Trace().
|
||||||
|
Int("status", resp.StatusCode).
|
||||||
|
Str("push_token", token).
|
||||||
|
Msg("Sent push request")
|
||||||
|
}
|
||||||
|
if resp != nil {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}
|
||||||
|
}
|
169
pkg/gomuks/pushmessage.go
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
// gomuks - A Matrix client written in Go.
|
||||||
|
// Copyright (C) 2025 Tulir Asokan
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package gomuks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"go.mau.fi/util/jsontime"
|
||||||
|
"go.mau.fi/util/ptr"
|
||||||
|
"maunium.net/go/mautrix/event"
|
||||||
|
"maunium.net/go/mautrix/id"
|
||||||
|
|
||||||
|
"go.mau.fi/gomuks/pkg/hicli"
|
||||||
|
"go.mau.fi/gomuks/pkg/hicli/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PushNewMessage struct {
|
||||||
|
Timestamp jsontime.UnixMilli `json:"timestamp"`
|
||||||
|
EventID id.EventID `json:"event_id"`
|
||||||
|
EventRowID database.EventRowID `json:"event_rowid"`
|
||||||
|
|
||||||
|
RoomID id.RoomID `json:"room_id"`
|
||||||
|
RoomName string `json:"room_name"`
|
||||||
|
RoomAvatar string `json:"room_avatar,omitempty"`
|
||||||
|
Sender NotificationUser `json:"sender"`
|
||||||
|
Self NotificationUser `json:"self"`
|
||||||
|
|
||||||
|
Text string `json:"text"`
|
||||||
|
Image string `json:"image,omitempty"`
|
||||||
|
Mention bool `json:"mention,omitempty"`
|
||||||
|
Reply bool `json:"reply,omitempty"`
|
||||||
|
Sound bool `json:"sound,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotificationUser struct {
|
||||||
|
ID id.UserID `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Avatar string `json:"avatar,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAvatarLinkForNotification(name, ident string, uri id.ContentURIString) string {
|
||||||
|
parsed := uri.ParseOrIgnore()
|
||||||
|
if !parsed.IsValid() {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var fallbackChar rune
|
||||||
|
if name == "" {
|
||||||
|
fallbackChar, _ = utf8.DecodeRuneInString(ident[1:])
|
||||||
|
} else {
|
||||||
|
fallbackChar, _ = utf8.DecodeRuneInString(name)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("_gomuks/media/%s/%s?encrypted=false&fallback=%s", parsed.Homeserver, parsed.FileID, url.QueryEscape(string(fallbackChar)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gmx *Gomuks) getNotificationUser(ctx context.Context, roomID id.RoomID, userID id.UserID) (user NotificationUser) {
|
||||||
|
user = NotificationUser{ID: userID, Name: userID.Localpart()}
|
||||||
|
memberEvt, err := gmx.Client.DB.CurrentState.Get(ctx, roomID, event.StateMember, userID.String())
|
||||||
|
if err != nil {
|
||||||
|
zerolog.Ctx(ctx).Err(err).Stringer("of_user_id", userID).Msg("Failed to get member event")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var memberContent event.MemberEventContent
|
||||||
|
_ = json.Unmarshal(memberEvt.Content, &memberContent)
|
||||||
|
if memberContent.Displayname != "" {
|
||||||
|
user.Name = memberContent.Displayname
|
||||||
|
}
|
||||||
|
if len(user.Name) > 50 {
|
||||||
|
user.Name = user.Name[:50] + "…"
|
||||||
|
}
|
||||||
|
if memberContent.AvatarURL != "" {
|
||||||
|
user.Avatar = getAvatarLinkForNotification(memberContent.Displayname, userID.String(), memberContent.AvatarURL)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gmx *Gomuks) formatPushNotificationMessage(ctx context.Context, notif hicli.SyncNotification) *PushNewMessage {
|
||||||
|
evtType := notif.Event.Type
|
||||||
|
rawContent := notif.Event.Content
|
||||||
|
if evtType == event.EventEncrypted.Type {
|
||||||
|
evtType = notif.Event.DecryptedType
|
||||||
|
rawContent = notif.Event.Decrypted
|
||||||
|
}
|
||||||
|
if evtType != event.EventMessage.Type && evtType != event.EventSticker.Type {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var content event.MessageEventContent
|
||||||
|
err := json.Unmarshal(rawContent, &content)
|
||||||
|
if err != nil {
|
||||||
|
zerolog.Ctx(ctx).Warn().Err(err).
|
||||||
|
Stringer("event_id", notif.Event.ID).
|
||||||
|
Msg("Failed to unmarshal message content to format push notification")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var roomAvatar, image string
|
||||||
|
if notif.Room.Avatar != nil {
|
||||||
|
avatarIdent := notif.Room.ID.String()
|
||||||
|
if ptr.Val(notif.Room.DMUserID) != "" {
|
||||||
|
avatarIdent = notif.Room.DMUserID.String()
|
||||||
|
}
|
||||||
|
roomAvatar = getAvatarLinkForNotification(ptr.Val(notif.Room.Name), avatarIdent, notif.Room.Avatar.CUString())
|
||||||
|
}
|
||||||
|
roomName := ptr.Val(notif.Room.Name)
|
||||||
|
if roomName == "" {
|
||||||
|
roomName = "Unnamed room"
|
||||||
|
}
|
||||||
|
if len(roomName) > 50 {
|
||||||
|
roomName = roomName[:50] + "…"
|
||||||
|
}
|
||||||
|
text := content.Body
|
||||||
|
if len(text) > 400 {
|
||||||
|
text = text[:350] + "[…]"
|
||||||
|
}
|
||||||
|
if content.MsgType == event.MsgImage || evtType == event.EventSticker.Type {
|
||||||
|
if content.File != nil && content.File.URL != "" {
|
||||||
|
parsed := content.File.URL.ParseOrIgnore()
|
||||||
|
if len(content.File.URL) < 255 && parsed.IsValid() {
|
||||||
|
image = fmt.Sprintf("_gomuks/media/%s/%s?encrypted=true", parsed.Homeserver, parsed.FileID)
|
||||||
|
}
|
||||||
|
} else if content.URL != "" {
|
||||||
|
parsed := content.URL.ParseOrIgnore()
|
||||||
|
if len(content.URL) < 255 && parsed.IsValid() {
|
||||||
|
image = fmt.Sprintf("_gomuks/media/%s/%s?encrypted=false", parsed.Homeserver, parsed.FileID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if content.FileName == "" || content.FileName == content.Body {
|
||||||
|
text = "Sent a photo"
|
||||||
|
}
|
||||||
|
} else if content.MsgType.IsMedia() {
|
||||||
|
if content.FileName == "" || content.FileName == content.Body {
|
||||||
|
text = "Sent a file: " + text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &PushNewMessage{
|
||||||
|
Timestamp: notif.Event.Timestamp,
|
||||||
|
EventID: notif.Event.ID,
|
||||||
|
EventRowID: notif.Event.RowID,
|
||||||
|
|
||||||
|
RoomID: notif.Room.ID,
|
||||||
|
RoomName: roomName,
|
||||||
|
RoomAvatar: roomAvatar,
|
||||||
|
Sender: gmx.getNotificationUser(ctx, notif.Room.ID, notif.Event.Sender),
|
||||||
|
Self: gmx.getNotificationUser(ctx, notif.Room.ID, gmx.Client.Account.UserID),
|
||||||
|
|
||||||
|
Text: text,
|
||||||
|
Image: image,
|
||||||
|
Mention: content.Mentions.Has(gmx.Client.Account.UserID),
|
||||||
|
Reply: content.RelatesTo.GetNonFallbackReplyTo() != "",
|
||||||
|
Sound: notif.Sound,
|
||||||
|
}
|
||||||
|
}
|
|
@ -190,10 +190,10 @@ func (gmx *Gomuks) generateToken() (string, time.Time) {
|
||||||
}), expiry
|
}), expiry
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gmx *Gomuks) generateImageToken() string {
|
func (gmx *Gomuks) generateImageToken(expiry time.Duration) string {
|
||||||
return gmx.signToken(tokenData{
|
return gmx.signToken(tokenData{
|
||||||
Username: gmx.Config.Web.Username,
|
Username: gmx.Config.Web.Username,
|
||||||
Expiry: jsontime.U(time.Now().Add(1 * time.Hour)),
|
Expiry: jsontime.U(time.Now().Add(expiry)),
|
||||||
ImageOnly: true,
|
ImageOnly: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -206,16 +206,26 @@ func (gmx *Gomuks) signToken(td any) string {
|
||||||
return base64.RawURLEncoding.EncodeToString(data) + "." + base64.RawURLEncoding.EncodeToString(checksum)
|
return base64.RawURLEncoding.EncodeToString(data) + "." + base64.RawURLEncoding.EncodeToString(checksum)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gmx *Gomuks) writeTokenCookie(w http.ResponseWriter) {
|
func (gmx *Gomuks) writeTokenCookie(w http.ResponseWriter, created, jsonOutput bool) {
|
||||||
token, expiry := gmx.generateToken()
|
token, expiry := gmx.generateToken()
|
||||||
http.SetCookie(w, &http.Cookie{
|
if !jsonOutput {
|
||||||
Name: "gomuks_auth",
|
http.SetCookie(w, &http.Cookie{
|
||||||
Value: token,
|
Name: "gomuks_auth",
|
||||||
Expires: expiry,
|
Value: token,
|
||||||
HttpOnly: true,
|
Expires: expiry,
|
||||||
Secure: true,
|
HttpOnly: true,
|
||||||
SameSite: http.SameSiteLaxMode,
|
Secure: true,
|
||||||
})
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if created {
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
if jsonOutput {
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]string{"token": token})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gmx *Gomuks) Authenticate(w http.ResponseWriter, r *http.Request) {
|
func (gmx *Gomuks) Authenticate(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -226,14 +236,17 @@ func (gmx *Gomuks) Authenticate(w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(http.StatusForbidden)
|
w.WriteHeader(http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
jsonOutput := r.URL.Query().Get("output") == "json"
|
||||||
|
allowPrompt := r.URL.Query().Get("no_prompt") != "true"
|
||||||
authCookie, err := r.Cookie("gomuks_auth")
|
authCookie, err := r.Cookie("gomuks_auth")
|
||||||
if err == nil && gmx.validateAuth(authCookie.Value, false) {
|
if err == nil && gmx.validateAuth(authCookie.Value, false) {
|
||||||
hlog.FromRequest(r).Debug().Msg("Authentication successful with existing cookie")
|
hlog.FromRequest(r).Debug().Msg("Authentication successful with existing cookie")
|
||||||
gmx.writeTokenCookie(w)
|
gmx.writeTokenCookie(w, false, jsonOutput)
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
} else if username, password, ok := r.BasicAuth(); !ok {
|
} else if username, password, ok := r.BasicAuth(); !ok {
|
||||||
hlog.FromRequest(r).Debug().Msg("Requesting credentials for auth request")
|
hlog.FromRequest(r).Debug().Msg("Requesting credentials for auth request")
|
||||||
w.Header().Set("WWW-Authenticate", `Basic realm="gomuks web" charset="UTF-8"`)
|
if allowPrompt {
|
||||||
|
w.Header().Set("WWW-Authenticate", `Basic realm="gomuks web" charset="UTF-8"`)
|
||||||
|
}
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
} else {
|
} else {
|
||||||
usernameHash := sha256.Sum256([]byte(username))
|
usernameHash := sha256.Sum256([]byte(username))
|
||||||
|
@ -242,11 +255,12 @@ func (gmx *Gomuks) Authenticate(w http.ResponseWriter, r *http.Request) {
|
||||||
passwordCorrect := bcrypt.CompareHashAndPassword([]byte(gmx.Config.Web.PasswordHash), []byte(password)) == nil
|
passwordCorrect := bcrypt.CompareHashAndPassword([]byte(gmx.Config.Web.PasswordHash), []byte(password)) == nil
|
||||||
if usernameCorrect && passwordCorrect {
|
if usernameCorrect && passwordCorrect {
|
||||||
hlog.FromRequest(r).Debug().Msg("Authentication successful with username and password")
|
hlog.FromRequest(r).Debug().Msg("Authentication successful with username and password")
|
||||||
gmx.writeTokenCookie(w)
|
gmx.writeTokenCookie(w, true, jsonOutput)
|
||||||
w.WriteHeader(http.StatusCreated)
|
|
||||||
} else {
|
} else {
|
||||||
hlog.FromRequest(r).Debug().Msg("Authentication failed with username and password, re-requesting credentials")
|
hlog.FromRequest(r).Debug().Msg("Authentication failed with username and password, re-requesting credentials")
|
||||||
w.Header().Set("WWW-Authenticate", `Basic realm="gomuks web" charset="UTF-8"`)
|
if allowPrompt {
|
||||||
|
w.Header().Set("WWW-Authenticate", `Basic realm="gomuks web" charset="UTF-8"`)
|
||||||
|
}
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,7 +86,7 @@ func (gmx *Gomuks) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||||
defer recoverPanic("read loop")
|
defer recoverPanic("read loop")
|
||||||
|
|
||||||
conn, acceptErr := websocket.Accept(w, r, &websocket.AcceptOptions{
|
conn, acceptErr := websocket.Accept(w, r, &websocket.AcceptOptions{
|
||||||
OriginPatterns: []string{"localhost:*"},
|
OriginPatterns: gmx.Config.Web.OriginPatterns,
|
||||||
})
|
})
|
||||||
if acceptErr != nil {
|
if acceptErr != nil {
|
||||||
log.Warn().Err(acceptErr).Msg("Failed to accept websocket connection")
|
log.Warn().Err(acceptErr).Msg("Failed to accept websocket connection")
|
||||||
|
@ -148,7 +148,7 @@ func (gmx *Gomuks) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||||
sendImageAuthToken := func() {
|
sendImageAuthToken := func() {
|
||||||
err := writeCmd(ctx, conn, &hicli.JSONCommand{
|
err := writeCmd(ctx, conn, &hicli.JSONCommand{
|
||||||
Command: "image_auth_token",
|
Command: "image_auth_token",
|
||||||
Data: exerrors.Must(json.Marshal(gmx.generateImageToken())),
|
Data: exerrors.Must(json.Marshal(gmx.generateImageToken(1 * time.Hour))),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("Failed to write image auth token message")
|
log.Err(err).Msg("Failed to write image auth token message")
|
||||||
|
|
|
@ -17,15 +17,18 @@ import (
|
||||||
type Database struct {
|
type Database struct {
|
||||||
*dbutil.Database
|
*dbutil.Database
|
||||||
|
|
||||||
Account AccountQuery
|
Account *AccountQuery
|
||||||
AccountData AccountDataQuery
|
AccountData *AccountDataQuery
|
||||||
Room RoomQuery
|
Room *RoomQuery
|
||||||
Event EventQuery
|
InvitedRoom *InvitedRoomQuery
|
||||||
CurrentState CurrentStateQuery
|
Event *EventQuery
|
||||||
Timeline TimelineQuery
|
CurrentState *CurrentStateQuery
|
||||||
SessionRequest SessionRequestQuery
|
Timeline *TimelineQuery
|
||||||
Receipt ReceiptQuery
|
SessionRequest *SessionRequestQuery
|
||||||
Media MediaQuery
|
Receipt *ReceiptQuery
|
||||||
|
Media *MediaQuery
|
||||||
|
SpaceEdge *SpaceEdgeQuery
|
||||||
|
PushRegistration *PushRegistrationQuery
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(rawDB *dbutil.Database) *Database {
|
func New(rawDB *dbutil.Database) *Database {
|
||||||
|
@ -34,15 +37,18 @@ func New(rawDB *dbutil.Database) *Database {
|
||||||
return &Database{
|
return &Database{
|
||||||
Database: rawDB,
|
Database: rawDB,
|
||||||
|
|
||||||
Account: AccountQuery{QueryHelper: dbutil.MakeQueryHelper(rawDB, newAccount)},
|
Account: &AccountQuery{QueryHelper: dbutil.MakeQueryHelper(rawDB, newAccount)},
|
||||||
AccountData: AccountDataQuery{QueryHelper: dbutil.MakeQueryHelper(rawDB, newAccountData)},
|
AccountData: &AccountDataQuery{QueryHelper: dbutil.MakeQueryHelper(rawDB, newAccountData)},
|
||||||
Room: RoomQuery{QueryHelper: dbutil.MakeQueryHelper(rawDB, newRoom)},
|
Room: &RoomQuery{QueryHelper: dbutil.MakeQueryHelper(rawDB, newRoom)},
|
||||||
Event: EventQuery{QueryHelper: eventQH},
|
InvitedRoom: &InvitedRoomQuery{QueryHelper: dbutil.MakeQueryHelper(rawDB, newInvitedRoom)},
|
||||||
CurrentState: CurrentStateQuery{QueryHelper: eventQH},
|
Event: &EventQuery{QueryHelper: eventQH},
|
||||||
Timeline: TimelineQuery{QueryHelper: eventQH},
|
CurrentState: &CurrentStateQuery{QueryHelper: eventQH},
|
||||||
SessionRequest: SessionRequestQuery{QueryHelper: dbutil.MakeQueryHelper(rawDB, newSessionRequest)},
|
Timeline: &TimelineQuery{QueryHelper: eventQH},
|
||||||
Receipt: ReceiptQuery{QueryHelper: dbutil.MakeQueryHelper(rawDB, newReceipt)},
|
SessionRequest: &SessionRequestQuery{QueryHelper: dbutil.MakeQueryHelper(rawDB, newSessionRequest)},
|
||||||
Media: MediaQuery{QueryHelper: dbutil.MakeQueryHelper(rawDB, newMedia)},
|
Receipt: &ReceiptQuery{QueryHelper: dbutil.MakeQueryHelper(rawDB, newReceipt)},
|
||||||
|
Media: &MediaQuery{QueryHelper: dbutil.MakeQueryHelper(rawDB, newMedia)},
|
||||||
|
SpaceEdge: &SpaceEdgeQuery{QueryHelper: dbutil.MakeQueryHelper(rawDB, newSpaceEdge)},
|
||||||
|
PushRegistration: &PushRegistrationQuery{QueryHelper: dbutil.MakeQueryHelper(rawDB, newPushRegistration)},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,6 +64,10 @@ func newRoom(_ *dbutil.QueryHelper[*Room]) *Room {
|
||||||
return &Room{}
|
return &Room{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newInvitedRoom(_ *dbutil.QueryHelper[*InvitedRoom]) *InvitedRoom {
|
||||||
|
return &InvitedRoom{}
|
||||||
|
}
|
||||||
|
|
||||||
func newReceipt(_ *dbutil.QueryHelper[*Receipt]) *Receipt {
|
func newReceipt(_ *dbutil.QueryHelper[*Receipt]) *Receipt {
|
||||||
return &Receipt{}
|
return &Receipt{}
|
||||||
}
|
}
|
||||||
|
@ -73,3 +83,11 @@ func newAccountData(_ *dbutil.QueryHelper[*AccountData]) *AccountData {
|
||||||
func newAccount(_ *dbutil.QueryHelper[*Account]) *Account {
|
func newAccount(_ *dbutil.QueryHelper[*Account]) *Account {
|
||||||
return &Account{}
|
return &Account{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newSpaceEdge(_ *dbutil.QueryHelper[*SpaceEdge]) *SpaceEdge {
|
||||||
|
return &SpaceEdge{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPushRegistration(_ *dbutil.QueryHelper[*PushRegistration]) *PushRegistration {
|
||||||
|
return &PushRegistration{}
|
||||||
|
}
|
||||||
|
|
|
@ -327,12 +327,17 @@ func (m EventRowID) GetMassInsertValues() [1]any {
|
||||||
}
|
}
|
||||||
|
|
||||||
type LocalContent struct {
|
type LocalContent struct {
|
||||||
SanitizedHTML string `json:"sanitized_html,omitempty"`
|
SanitizedHTML string `json:"sanitized_html,omitempty"`
|
||||||
HTMLVersion int `json:"html_version,omitempty"`
|
HTMLVersion int `json:"html_version,omitempty"`
|
||||||
WasPlaintext bool `json:"was_plaintext,omitempty"`
|
WasPlaintext bool `json:"was_plaintext,omitempty"`
|
||||||
BigEmoji bool `json:"big_emoji,omitempty"`
|
BigEmoji bool `json:"big_emoji,omitempty"`
|
||||||
HasMath bool `json:"has_math,omitempty"`
|
HasMath bool `json:"has_math,omitempty"`
|
||||||
EditSource string `json:"edit_source,omitempty"`
|
EditSource string `json:"edit_source,omitempty"`
|
||||||
|
ReplyFallbackRemoved bool `json:"reply_fallback_removed,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LocalContent) GetReplyFallbackRemoved() bool {
|
||||||
|
return c != nil && c.ReplyFallbackRemoved
|
||||||
}
|
}
|
||||||
|
|
||||||
type Event struct {
|
type Event struct {
|
||||||
|
@ -461,6 +466,7 @@ func (e *Event) Scan(row dbutil.Scannable) (*Event, error) {
|
||||||
|
|
||||||
var relatesToPath = exgjson.Path("m.relates_to", "event_id")
|
var relatesToPath = exgjson.Path("m.relates_to", "event_id")
|
||||||
var relationTypePath = exgjson.Path("m.relates_to", "rel_type")
|
var relationTypePath = exgjson.Path("m.relates_to", "rel_type")
|
||||||
|
var replyToPath = exgjson.Path("m.relates_to", "m.in_reply_to", "event_id")
|
||||||
|
|
||||||
func getRelatesToFromEvent(evt *event.Event) (id.EventID, event.RelationType) {
|
func getRelatesToFromEvent(evt *event.Event) (id.EventID, event.RelationType) {
|
||||||
if evt.StateKey != nil {
|
if evt.StateKey != nil {
|
||||||
|
@ -488,6 +494,18 @@ func getMegolmSessionID(evt *event.Event) id.SessionID {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *Event) GetReplyTo() id.EventID {
|
||||||
|
content := e.Content
|
||||||
|
if e.Decrypted != nil {
|
||||||
|
content = e.Decrypted
|
||||||
|
}
|
||||||
|
result := gjson.GetBytes(content, replyToPath)
|
||||||
|
if result.Type == gjson.String {
|
||||||
|
return id.EventID(result.Str)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func (e *Event) sqlVariables() []any {
|
func (e *Event) sqlVariables() []any {
|
||||||
var reactions any
|
var reactions any
|
||||||
if e.Reactions != nil {
|
if e.Reactions != nil {
|
||||||
|
@ -545,3 +563,10 @@ func (e *Event) BumpsSortingTimestamp() bool {
|
||||||
return (e.Type == event.EventMessage.Type || e.Type == event.EventSticker.Type || e.Type == event.EventEncrypted.Type) &&
|
return (e.Type == event.EventMessage.Type || e.Type == event.EventSticker.Type || e.Type == event.EventEncrypted.Type) &&
|
||||||
e.RelationType != event.RelReplace
|
e.RelationType != event.RelReplace
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *Event) MarkReplyFallbackRemoved() {
|
||||||
|
if e.LocalContent == nil {
|
||||||
|
e.LocalContent = &LocalContent{}
|
||||||
|
}
|
||||||
|
e.LocalContent.ReplyFallbackRemoved = true
|
||||||
|
}
|
||||||
|
|
73
pkg/hicli/database/invitedroom.go
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
// Copyright (c) 2024 Tulir Asokan
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"go.mau.fi/util/dbutil"
|
||||||
|
"go.mau.fi/util/jsontime"
|
||||||
|
"maunium.net/go/mautrix/event"
|
||||||
|
"maunium.net/go/mautrix/id"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
getInvitedRoomsQuery = `
|
||||||
|
SELECT room_id, received_at, invite_state
|
||||||
|
FROM invited_room
|
||||||
|
ORDER BY received_at DESC
|
||||||
|
`
|
||||||
|
deleteInvitedRoomQuery = `
|
||||||
|
DELETE FROM invited_room WHERE room_id = $1
|
||||||
|
`
|
||||||
|
upsertInvitedRoomQuery = `
|
||||||
|
INSERT INTO invited_room (room_id, received_at, invite_state)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
ON CONFLICT (room_id) DO UPDATE
|
||||||
|
SET received_at = $2, invite_state = $3
|
||||||
|
`
|
||||||
|
)
|
||||||
|
|
||||||
|
type InvitedRoomQuery struct {
|
||||||
|
*dbutil.QueryHelper[*InvitedRoom]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (irq *InvitedRoomQuery) GetAll(ctx context.Context) ([]*InvitedRoom, error) {
|
||||||
|
return irq.QueryMany(ctx, getInvitedRoomsQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (irq *InvitedRoomQuery) Upsert(ctx context.Context, room *InvitedRoom) error {
|
||||||
|
return irq.Exec(ctx, upsertInvitedRoomQuery, room.sqlVariables()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (irq *InvitedRoomQuery) Delete(ctx context.Context, roomID id.RoomID) error {
|
||||||
|
return irq.Exec(ctx, deleteInvitedRoomQuery, roomID)
|
||||||
|
}
|
||||||
|
|
||||||
|
type InvitedRoom struct {
|
||||||
|
ID id.RoomID `json:"room_id"`
|
||||||
|
CreatedAt jsontime.UnixMilli `json:"created_at"`
|
||||||
|
InviteState []*event.Event `json:"invite_state"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *InvitedRoom) sqlVariables() []any {
|
||||||
|
return []any{
|
||||||
|
r.ID,
|
||||||
|
dbutil.UnixMilliPtr(r.CreatedAt.Time),
|
||||||
|
dbutil.JSON{Data: &r.InviteState},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *InvitedRoom) Scan(row dbutil.Scannable) (*InvitedRoom, error) {
|
||||||
|
var createdAt int64
|
||||||
|
err := row.Scan(&r.ID, &createdAt, dbutil.JSON{Data: &r.InviteState})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
r.CreatedAt = jsontime.UMInt(createdAt)
|
||||||
|
return r, nil
|
||||||
|
}
|
78
pkg/hicli/database/pushregistration.go
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
// Copyright (c) 2025 Tulir Asokan
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.mau.fi/util/dbutil"
|
||||||
|
"go.mau.fi/util/jsontime"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
getNonExpiredPushTargets = `
|
||||||
|
SELECT device_id, type, data, encryption, expiration
|
||||||
|
FROM push_registration
|
||||||
|
WHERE expiration > $1
|
||||||
|
`
|
||||||
|
putPushRegistration = `
|
||||||
|
INSERT INTO push_registration (device_id, type, data, encryption, expiration)
|
||||||
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
|
ON CONFLICT (device_id) DO UPDATE SET
|
||||||
|
type = EXCLUDED.type,
|
||||||
|
data = EXCLUDED.data,
|
||||||
|
encryption = EXCLUDED.encryption,
|
||||||
|
expiration = EXCLUDED.expiration
|
||||||
|
`
|
||||||
|
)
|
||||||
|
|
||||||
|
type PushRegistrationQuery struct {
|
||||||
|
*dbutil.QueryHelper[*PushRegistration]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (prq *PushRegistrationQuery) Put(ctx context.Context, reg *PushRegistration) error {
|
||||||
|
return prq.Exec(ctx, putPushRegistration, reg.sqlVariables()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (seq *PushRegistrationQuery) GetAll(ctx context.Context) ([]*PushRegistration, error) {
|
||||||
|
return seq.QueryMany(ctx, getNonExpiredPushTargets, time.Now().Unix())
|
||||||
|
}
|
||||||
|
|
||||||
|
type PushType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PushTypeFCM PushType = "fcm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EncryptionKey struct {
|
||||||
|
Key []byte `json:"key,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PushRegistration struct {
|
||||||
|
DeviceID string `json:"device_id"`
|
||||||
|
Type PushType `json:"type"`
|
||||||
|
Data json.RawMessage `json:"data"`
|
||||||
|
Encryption EncryptionKey `json:"encryption"`
|
||||||
|
Expiration jsontime.Unix `json:"expiration"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pe *PushRegistration) Scan(row dbutil.Scannable) (*PushRegistration, error) {
|
||||||
|
err := row.Scan(&pe.DeviceID, &pe.Type, (*[]byte)(&pe.Data), dbutil.JSON{Data: &pe.Encryption}, &pe.Expiration)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return pe, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pe *PushRegistration) sqlVariables() []any {
|
||||||
|
if pe.Expiration.IsZero() {
|
||||||
|
pe.Expiration = jsontime.U(time.Now().Add(7 * 24 * time.Hour))
|
||||||
|
}
|
||||||
|
return []interface{}{pe.DeviceID, pe.Type, unsafeJSONString(pe.Data), dbutil.JSON{Data: &pe.Encryption}, pe.Expiration}
|
||||||
|
}
|
|
@ -8,7 +8,9 @@ package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.mau.fi/util/dbutil"
|
"go.mau.fi/util/dbutil"
|
||||||
|
@ -25,6 +27,7 @@ const (
|
||||||
SET event_id = excluded.event_id,
|
SET event_id = excluded.event_id,
|
||||||
timestamp = excluded.timestamp
|
timestamp = excluded.timestamp
|
||||||
`
|
`
|
||||||
|
getReadReceiptsQuery = `SELECT room_id, user_id, receipt_type, thread_id, event_id, timestamp FROM receipt WHERE room_id = $1 AND receipt_type='m.read' AND event_id IN ($2)`
|
||||||
)
|
)
|
||||||
|
|
||||||
var receiptMassInserter = dbutil.NewMassInsertBuilder[*Receipt, [1]any](upsertReceiptQuery, "($1, $%d, $%d, $%d, $%d, $%d)")
|
var receiptMassInserter = dbutil.NewMassInsertBuilder[*Receipt, [1]any](upsertReceiptQuery, "($1, $%d, $%d, $%d, $%d, $%d)")
|
||||||
|
@ -53,11 +56,29 @@ func (rq *ReceiptQuery) PutMany(ctx context.Context, roomID id.RoomID, receipts
|
||||||
return rq.Exec(ctx, query, params...)
|
return rq.Exec(ctx, query, params...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (rq *ReceiptQuery) GetManyRead(ctx context.Context, roomID id.RoomID, eventIDs []id.EventID) (map[id.EventID][]*Receipt, error) {
|
||||||
|
args := make([]any, len(eventIDs)+1)
|
||||||
|
placeholders := make([]string, len(eventIDs)+1)
|
||||||
|
args[0] = roomID
|
||||||
|
placeholders[0] = "?1"
|
||||||
|
for i, evtID := range eventIDs {
|
||||||
|
args[i+1] = evtID
|
||||||
|
placeholders[i+1] = fmt.Sprintf("?%d", i+2)
|
||||||
|
}
|
||||||
|
query := strings.Replace(getReadReceiptsQuery, "$2", strings.Join(placeholders, ", "), 1)
|
||||||
|
output := make(map[id.EventID][]*Receipt)
|
||||||
|
err := rq.QueryManyIter(ctx, query, args...).Iter(func(receipt *Receipt) (bool, error) {
|
||||||
|
output[receipt.EventID] = append(output[receipt.EventID], receipt)
|
||||||
|
return true, nil
|
||||||
|
})
|
||||||
|
return output, err
|
||||||
|
}
|
||||||
|
|
||||||
type Receipt struct {
|
type Receipt struct {
|
||||||
RoomID id.RoomID `json:"room_id"`
|
RoomID id.RoomID `json:"room_id,omitempty"`
|
||||||
UserID id.UserID `json:"user_id"`
|
UserID id.UserID `json:"user_id"`
|
||||||
ReceiptType event.ReceiptType `json:"receipt_type"`
|
ReceiptType event.ReceiptType `json:"receipt_type"`
|
||||||
ThreadID event.ThreadID `json:"thread_id"`
|
ThreadID event.ThreadID `json:"thread_id,omitempty"`
|
||||||
EventID id.EventID `json:"event_id"`
|
EventID id.EventID `json:"event_id"`
|
||||||
Timestamp jsontime.UnixMilli `json:"timestamp"`
|
Timestamp jsontime.UnixMilli `json:"timestamp"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,12 +21,14 @@ import (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
getRoomBaseQuery = `
|
getRoomBaseQuery = `
|
||||||
SELECT room_id, creation_content, tombstone_content, name, name_quality, avatar, explicit_avatar, topic, canonical_alias,
|
SELECT room_id, creation_content, tombstone_content, name, name_quality,
|
||||||
|
avatar, explicit_avatar, dm_user_id, topic, canonical_alias,
|
||||||
lazy_load_summary, encryption_event, has_member_list, preview_event_rowid, sorting_timestamp,
|
lazy_load_summary, encryption_event, has_member_list, preview_event_rowid, sorting_timestamp,
|
||||||
unread_highlights, unread_notifications, unread_messages, marked_unread, prev_batch
|
unread_highlights, unread_notifications, unread_messages, marked_unread, prev_batch
|
||||||
FROM room
|
FROM room
|
||||||
`
|
`
|
||||||
getRoomsBySortingTimestampQuery = getRoomBaseQuery + `WHERE sorting_timestamp < $1 AND sorting_timestamp > 0 ORDER BY sorting_timestamp DESC LIMIT $2`
|
getRoomsBySortingTimestampQuery = getRoomBaseQuery + `WHERE sorting_timestamp < $1 AND sorting_timestamp > 0 ORDER BY sorting_timestamp DESC LIMIT $2`
|
||||||
|
getRoomsByTypeQuery = getRoomBaseQuery + `WHERE room_type = $1`
|
||||||
getRoomByIDQuery = getRoomBaseQuery + `WHERE room_id = $1`
|
getRoomByIDQuery = getRoomBaseQuery + `WHERE room_id = $1`
|
||||||
ensureRoomExistsQuery = `
|
ensureRoomExistsQuery = `
|
||||||
INSERT INTO room (room_id) VALUES ($1)
|
INSERT INTO room (room_id) VALUES ($1)
|
||||||
|
@ -34,24 +36,26 @@ const (
|
||||||
`
|
`
|
||||||
upsertRoomFromSyncQuery = `
|
upsertRoomFromSyncQuery = `
|
||||||
UPDATE room
|
UPDATE room
|
||||||
SET creation_content = COALESCE(room.creation_content, $2),
|
SET room_type = COALESCE(room.room_type, json($2)->>'$.type'),
|
||||||
|
creation_content = COALESCE(room.creation_content, $2),
|
||||||
tombstone_content = COALESCE(room.tombstone_content, $3),
|
tombstone_content = COALESCE(room.tombstone_content, $3),
|
||||||
name = COALESCE($4, room.name),
|
name = COALESCE($4, room.name),
|
||||||
name_quality = CASE WHEN $4 IS NOT NULL THEN $5 ELSE room.name_quality END,
|
name_quality = CASE WHEN $4 IS NOT NULL THEN $5 ELSE room.name_quality END,
|
||||||
avatar = COALESCE($6, room.avatar),
|
avatar = COALESCE($6, room.avatar),
|
||||||
explicit_avatar = CASE WHEN $6 IS NOT NULL THEN $7 ELSE room.explicit_avatar END,
|
explicit_avatar = CASE WHEN $6 IS NOT NULL THEN $7 ELSE room.explicit_avatar END,
|
||||||
topic = COALESCE($8, room.topic),
|
dm_user_id = COALESCE($8, room.dm_user_id),
|
||||||
canonical_alias = COALESCE($9, room.canonical_alias),
|
topic = COALESCE($9, room.topic),
|
||||||
lazy_load_summary = COALESCE($10, room.lazy_load_summary),
|
canonical_alias = COALESCE($10, room.canonical_alias),
|
||||||
encryption_event = COALESCE($11, room.encryption_event),
|
lazy_load_summary = COALESCE($11, room.lazy_load_summary),
|
||||||
has_member_list = room.has_member_list OR $12,
|
encryption_event = COALESCE($12, room.encryption_event),
|
||||||
preview_event_rowid = COALESCE($13, room.preview_event_rowid),
|
has_member_list = room.has_member_list OR $13,
|
||||||
sorting_timestamp = COALESCE($14, room.sorting_timestamp),
|
preview_event_rowid = COALESCE($14, room.preview_event_rowid),
|
||||||
unread_highlights = COALESCE($15, room.unread_highlights),
|
sorting_timestamp = COALESCE($15, room.sorting_timestamp),
|
||||||
unread_notifications = COALESCE($16, room.unread_notifications),
|
unread_highlights = COALESCE($16, room.unread_highlights),
|
||||||
unread_messages = COALESCE($17, room.unread_messages),
|
unread_notifications = COALESCE($17, room.unread_notifications),
|
||||||
marked_unread = COALESCE($18, room.marked_unread),
|
unread_messages = COALESCE($18, room.unread_messages),
|
||||||
prev_batch = COALESCE($19, room.prev_batch)
|
marked_unread = COALESCE($19, room.marked_unread),
|
||||||
|
prev_batch = COALESCE($20, room.prev_batch)
|
||||||
WHERE room_id = $1
|
WHERE room_id = $1
|
||||||
`
|
`
|
||||||
setRoomPrevBatchQuery = `
|
setRoomPrevBatchQuery = `
|
||||||
|
@ -95,6 +99,10 @@ func (rq *RoomQuery) GetBySortTS(ctx context.Context, maxTS time.Time, limit int
|
||||||
return rq.QueryMany(ctx, getRoomsBySortingTimestampQuery, maxTS.UnixMilli(), limit)
|
return rq.QueryMany(ctx, getRoomsBySortingTimestampQuery, maxTS.UnixMilli(), limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (rq *RoomQuery) GetAllSpaces(ctx context.Context) ([]*Room, error) {
|
||||||
|
return rq.QueryMany(ctx, getRoomsByTypeQuery, event.RoomTypeSpace)
|
||||||
|
}
|
||||||
|
|
||||||
func (rq *RoomQuery) Upsert(ctx context.Context, room *Room) error {
|
func (rq *RoomQuery) Upsert(ctx context.Context, room *Room) error {
|
||||||
return rq.Exec(ctx, upsertRoomFromSyncQuery, room.sqlVariables()...)
|
return rq.Exec(ctx, upsertRoomFromSyncQuery, room.sqlVariables()...)
|
||||||
}
|
}
|
||||||
|
@ -147,6 +155,7 @@ type Room struct {
|
||||||
NameQuality NameQuality `json:"name_quality"`
|
NameQuality NameQuality `json:"name_quality"`
|
||||||
Avatar *id.ContentURI `json:"avatar,omitempty"`
|
Avatar *id.ContentURI `json:"avatar,omitempty"`
|
||||||
ExplicitAvatar bool `json:"explicit_avatar"`
|
ExplicitAvatar bool `json:"explicit_avatar"`
|
||||||
|
DMUserID *id.UserID `json:"dm_user_id,omitempty"`
|
||||||
Topic *string `json:"topic,omitempty"`
|
Topic *string `json:"topic,omitempty"`
|
||||||
CanonicalAlias *id.RoomAlias `json:"canonical_alias,omitempty"`
|
CanonicalAlias *id.RoomAlias `json:"canonical_alias,omitempty"`
|
||||||
|
|
||||||
|
@ -182,6 +191,10 @@ func (r *Room) CheckChangesAndCopyInto(other *Room) (hasChanges bool) {
|
||||||
other.ExplicitAvatar = r.ExplicitAvatar
|
other.ExplicitAvatar = r.ExplicitAvatar
|
||||||
hasChanges = true
|
hasChanges = true
|
||||||
}
|
}
|
||||||
|
if r.DMUserID != nil {
|
||||||
|
other.DMUserID = r.DMUserID
|
||||||
|
hasChanges = true
|
||||||
|
}
|
||||||
if r.Topic != nil {
|
if r.Topic != nil {
|
||||||
other.Topic = r.Topic
|
other.Topic = r.Topic
|
||||||
hasChanges = true
|
hasChanges = true
|
||||||
|
@ -244,6 +257,7 @@ func (r *Room) Scan(row dbutil.Scannable) (*Room, error) {
|
||||||
&r.NameQuality,
|
&r.NameQuality,
|
||||||
&r.Avatar,
|
&r.Avatar,
|
||||||
&r.ExplicitAvatar,
|
&r.ExplicitAvatar,
|
||||||
|
&r.DMUserID,
|
||||||
&r.Topic,
|
&r.Topic,
|
||||||
&r.CanonicalAlias,
|
&r.CanonicalAlias,
|
||||||
dbutil.JSON{Data: &r.LazyLoadSummary},
|
dbutil.JSON{Data: &r.LazyLoadSummary},
|
||||||
|
@ -262,7 +276,7 @@ func (r *Room) Scan(row dbutil.Scannable) (*Room, error) {
|
||||||
}
|
}
|
||||||
r.PrevBatch = prevBatch.String
|
r.PrevBatch = prevBatch.String
|
||||||
r.PreviewEventRowID = EventRowID(previewEventRowID.Int64)
|
r.PreviewEventRowID = EventRowID(previewEventRowID.Int64)
|
||||||
r.SortingTimestamp = jsontime.UM(time.UnixMilli(sortingTimestamp.Int64))
|
r.SortingTimestamp = jsontime.UMInt(sortingTimestamp.Int64)
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -275,6 +289,7 @@ func (r *Room) sqlVariables() []any {
|
||||||
r.NameQuality,
|
r.NameQuality,
|
||||||
r.Avatar,
|
r.Avatar,
|
||||||
r.ExplicitAvatar,
|
r.ExplicitAvatar,
|
||||||
|
r.DMUserID,
|
||||||
r.Topic,
|
r.Topic,
|
||||||
r.CanonicalAlias,
|
r.CanonicalAlias,
|
||||||
dbutil.JSONPtr(r.LazyLoadSummary),
|
dbutil.JSONPtr(r.LazyLoadSummary),
|
||||||
|
|
250
pkg/hicli/database/space.go
Normal file
|
@ -0,0 +1,250 @@
|
||||||
|
// Copyright (c) 2024 Tulir Asokan
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
"go.mau.fi/util/dbutil"
|
||||||
|
"maunium.net/go/mautrix/id"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
getAllSpaceChildren = `
|
||||||
|
SELECT space_id, child_id, child_event_rowid, "order", suggested, parent_event_rowid, canonical, parent_validated
|
||||||
|
FROM space_edge
|
||||||
|
-- This check should be redundant thanks to parent_validated and validation before insert for children
|
||||||
|
--INNER JOIN room ON space_id = room.room_id AND room.room_type = 'm.space'
|
||||||
|
WHERE (space_id = $1 OR $1 = '') AND (child_event_rowid IS NOT NULL OR parent_validated)
|
||||||
|
ORDER BY space_id, "order", child_id
|
||||||
|
`
|
||||||
|
getTopLevelSpaces = `
|
||||||
|
SELECT space_id
|
||||||
|
FROM (SELECT DISTINCT(space_id) FROM space_edge) outeredge
|
||||||
|
LEFT JOIN room_account_data ON
|
||||||
|
room_account_data.user_id = $1
|
||||||
|
AND room_account_data.room_id = outeredge.space_id
|
||||||
|
AND room_account_data.type = 'org.matrix.msc3230.space_order'
|
||||||
|
WHERE NOT EXISTS(
|
||||||
|
SELECT 1
|
||||||
|
FROM space_edge inneredge
|
||||||
|
INNER JOIN room ON inneredge.space_id = room.room_id
|
||||||
|
WHERE inneredge.child_id = outeredge.space_id
|
||||||
|
AND (inneredge.child_event_rowid IS NOT NULL OR inneredge.parent_validated)
|
||||||
|
) AND EXISTS(SELECT 1 FROM room WHERE room_id = space_id AND room_type = 'm.space')
|
||||||
|
ORDER BY room_account_data.content->>'$.order' NULLS LAST, space_id
|
||||||
|
`
|
||||||
|
revalidateAllParents = `
|
||||||
|
UPDATE space_edge
|
||||||
|
SET parent_validated=(SELECT EXISTS(
|
||||||
|
SELECT 1
|
||||||
|
FROM room
|
||||||
|
INNER JOIN current_state cs ON cs.room_id = room.room_id AND cs.event_type = 'm.room.power_levels' AND cs.state_key = ''
|
||||||
|
INNER JOIN event pls ON cs.event_rowid = pls.rowid
|
||||||
|
INNER JOIN event edgeevt ON space_edge.parent_event_rowid = edgeevt.rowid
|
||||||
|
WHERE room.room_id = space_edge.space_id
|
||||||
|
AND room.room_type = 'm.space'
|
||||||
|
AND COALESCE(
|
||||||
|
(
|
||||||
|
SELECT value
|
||||||
|
FROM json_each(pls.content, '$.users')
|
||||||
|
WHERE key=edgeevt.sender AND type='integer'
|
||||||
|
),
|
||||||
|
pls.content->>'$.users_default',
|
||||||
|
0
|
||||||
|
) >= COALESCE(
|
||||||
|
pls.content->>'$.events."m.space.child"',
|
||||||
|
pls.content->>'$.state_default',
|
||||||
|
50
|
||||||
|
)
|
||||||
|
))
|
||||||
|
WHERE parent_event_rowid IS NOT NULL
|
||||||
|
`
|
||||||
|
revalidateAllParentsPointingAtSpaceQuery = revalidateAllParents + ` AND space_id=$1`
|
||||||
|
revalidateAllParentsOfRoomQuery = revalidateAllParents + ` AND child_id=$1`
|
||||||
|
revalidateSpecificParentQuery = revalidateAllParents + ` AND space_id=$1 AND child_id=$2`
|
||||||
|
clearSpaceChildrenQuery = `
|
||||||
|
UPDATE space_edge SET child_event_rowid=NULL, "order"='', suggested=false
|
||||||
|
WHERE space_id=$1
|
||||||
|
`
|
||||||
|
clearSpaceParentsQuery = `
|
||||||
|
UPDATE space_edge SET parent_event_rowid=NULL, canonical=false, parent_validated=false
|
||||||
|
WHERE child_id=$1
|
||||||
|
`
|
||||||
|
removeSpaceChildQuery = clearSpaceChildrenQuery + ` AND child_id=$2`
|
||||||
|
removeSpaceParentQuery = clearSpaceParentsQuery + ` AND space_id=$2`
|
||||||
|
deleteEmptySpaceEdgeRowsQuery = `
|
||||||
|
DELETE FROM space_edge WHERE child_event_rowid IS NULL AND parent_event_rowid IS NULL
|
||||||
|
`
|
||||||
|
addSpaceChildQuery = `
|
||||||
|
INSERT INTO space_edge (space_id, child_id, child_event_rowid, "order", suggested)
|
||||||
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
|
ON CONFLICT (space_id, child_id) DO UPDATE
|
||||||
|
SET child_event_rowid=EXCLUDED.child_event_rowid,
|
||||||
|
"order"=EXCLUDED."order",
|
||||||
|
suggested=EXCLUDED.suggested
|
||||||
|
`
|
||||||
|
addSpaceParentQuery = `
|
||||||
|
INSERT INTO space_edge (space_id, child_id, parent_event_rowid, canonical)
|
||||||
|
VALUES ($1, $2, $3, $4)
|
||||||
|
ON CONFLICT (space_id, child_id) DO UPDATE
|
||||||
|
SET parent_event_rowid=EXCLUDED.parent_event_rowid,
|
||||||
|
canonical=EXCLUDED.canonical,
|
||||||
|
parent_validated=false
|
||||||
|
`
|
||||||
|
)
|
||||||
|
|
||||||
|
var massInsertSpaceParentBuilder = dbutil.NewMassInsertBuilder[SpaceParentEntry, [1]any](addSpaceParentQuery, "($%d, $1, $%d, $%d)")
|
||||||
|
var massInsertSpaceChildBuilder = dbutil.NewMassInsertBuilder[SpaceChildEntry, [1]any](addSpaceChildQuery, "($1, $%d, $%d, $%d, $%d)")
|
||||||
|
|
||||||
|
type SpaceEdgeQuery struct {
|
||||||
|
*dbutil.QueryHelper[*SpaceEdge]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (seq *SpaceEdgeQuery) AddChild(ctx context.Context, spaceID, childID id.RoomID, childEventRowID EventRowID, order string, suggested bool) error {
|
||||||
|
return seq.Exec(ctx, addSpaceChildQuery, spaceID, childID, childEventRowID, order, suggested)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (seq *SpaceEdgeQuery) AddParent(ctx context.Context, spaceID, childID id.RoomID, parentEventRowID EventRowID, canonical bool) error {
|
||||||
|
return seq.Exec(ctx, addSpaceParentQuery, spaceID, childID, parentEventRowID, canonical)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SpaceParentEntry struct {
|
||||||
|
ParentID id.RoomID
|
||||||
|
EventRowID EventRowID
|
||||||
|
Canonical bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (spe SpaceParentEntry) GetMassInsertValues() [3]any {
|
||||||
|
return [...]any{spe.ParentID, spe.EventRowID, spe.Canonical}
|
||||||
|
}
|
||||||
|
|
||||||
|
type SpaceChildEntry struct {
|
||||||
|
ChildID id.RoomID
|
||||||
|
EventRowID EventRowID
|
||||||
|
Order string
|
||||||
|
Suggested bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sce SpaceChildEntry) GetMassInsertValues() [4]any {
|
||||||
|
return [...]any{sce.ChildID, sce.EventRowID, sce.Order, sce.Suggested}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (seq *SpaceEdgeQuery) SetChildren(ctx context.Context, spaceID id.RoomID, children []SpaceChildEntry, removedChildren []id.RoomID, clear bool) error {
|
||||||
|
if clear {
|
||||||
|
err := seq.Exec(ctx, clearSpaceChildrenQuery, spaceID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if len(removedChildren) > 0 {
|
||||||
|
for _, child := range removedChildren {
|
||||||
|
err := seq.Exec(ctx, removeSpaceChildQuery, spaceID, child)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(removedChildren) > 0 {
|
||||||
|
err := seq.Exec(ctx, deleteEmptySpaceEdgeRowsQuery, spaceID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(children) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
query, params := massInsertSpaceChildBuilder.Build([1]any{spaceID}, children)
|
||||||
|
return seq.Exec(ctx, query, params...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (seq *SpaceEdgeQuery) SetParents(ctx context.Context, childID id.RoomID, parents []SpaceParentEntry, removedParents []id.RoomID, clear bool) error {
|
||||||
|
if clear {
|
||||||
|
err := seq.Exec(ctx, clearSpaceParentsQuery, childID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if len(removedParents) > 0 {
|
||||||
|
for _, parent := range removedParents {
|
||||||
|
err := seq.Exec(ctx, removeSpaceParentQuery, childID, parent)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(removedParents) > 0 {
|
||||||
|
err := seq.Exec(ctx, deleteEmptySpaceEdgeRowsQuery)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(parents) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
query, params := massInsertSpaceParentBuilder.Build([1]any{childID}, parents)
|
||||||
|
return seq.Exec(ctx, query, params...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (seq *SpaceEdgeQuery) RevalidateAllChildrenOfParentValidity(ctx context.Context, spaceID id.RoomID) error {
|
||||||
|
return seq.Exec(ctx, revalidateAllParentsPointingAtSpaceQuery, spaceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (seq *SpaceEdgeQuery) RevalidateAllParentsOfRoomValidity(ctx context.Context, childID id.RoomID) error {
|
||||||
|
return seq.Exec(ctx, revalidateAllParentsOfRoomQuery, childID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (seq *SpaceEdgeQuery) RevalidateSpecificParentValidity(ctx context.Context, spaceID, childID id.RoomID) error {
|
||||||
|
return seq.Exec(ctx, revalidateSpecificParentQuery, spaceID, childID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (seq *SpaceEdgeQuery) GetAll(ctx context.Context, spaceID id.RoomID) (map[id.RoomID][]*SpaceEdge, error) {
|
||||||
|
edges := make(map[id.RoomID][]*SpaceEdge)
|
||||||
|
err := seq.QueryManyIter(ctx, getAllSpaceChildren, spaceID).Iter(func(edge *SpaceEdge) (bool, error) {
|
||||||
|
edges[edge.SpaceID] = append(edges[edge.SpaceID], edge)
|
||||||
|
edge.SpaceID = ""
|
||||||
|
if !edge.ParentValidated {
|
||||||
|
edge.ParentEventRowID = 0
|
||||||
|
edge.Canonical = false
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
})
|
||||||
|
return edges, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var roomIDScanner = dbutil.ConvertRowFn[id.RoomID](dbutil.ScanSingleColumn[id.RoomID])
|
||||||
|
|
||||||
|
func (seq *SpaceEdgeQuery) GetTopLevelIDs(ctx context.Context, userID id.UserID) ([]id.RoomID, error) {
|
||||||
|
return roomIDScanner.NewRowIter(seq.GetDB().Query(ctx, getTopLevelSpaces, userID)).AsList()
|
||||||
|
}
|
||||||
|
|
||||||
|
type SpaceEdge struct {
|
||||||
|
SpaceID id.RoomID `json:"space_id,omitempty"`
|
||||||
|
ChildID id.RoomID `json:"child_id"`
|
||||||
|
|
||||||
|
ChildEventRowID EventRowID `json:"child_event_rowid,omitempty"`
|
||||||
|
Order string `json:"order,omitempty"`
|
||||||
|
Suggested bool `json:"suggested,omitempty"`
|
||||||
|
|
||||||
|
ParentEventRowID EventRowID `json:"parent_event_rowid,omitempty"`
|
||||||
|
Canonical bool `json:"canonical,omitempty"`
|
||||||
|
ParentValidated bool `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (se *SpaceEdge) Scan(row dbutil.Scannable) (*SpaceEdge, error) {
|
||||||
|
var childRowID, parentRowID sql.NullInt64
|
||||||
|
err := row.Scan(
|
||||||
|
&se.SpaceID, &se.ChildID,
|
||||||
|
&childRowID, &se.Order, &se.Suggested,
|
||||||
|
&parentRowID, &se.Canonical, &se.ParentValidated,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
se.ChildEventRowID = EventRowID(childRowID.Int64)
|
||||||
|
se.ParentEventRowID = EventRowID(parentRowID.Int64)
|
||||||
|
return se, nil
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
-- v0 -> v7 (compatible with v5+): Latest revision
|
-- v0 -> v12 (compatible with v10+): Latest revision
|
||||||
CREATE TABLE account (
|
CREATE TABLE account (
|
||||||
user_id TEXT NOT NULL PRIMARY KEY,
|
user_id TEXT NOT NULL PRIMARY KEY,
|
||||||
device_id TEXT NOT NULL,
|
device_id TEXT NOT NULL,
|
||||||
|
@ -10,6 +10,7 @@ CREATE TABLE account (
|
||||||
|
|
||||||
CREATE TABLE room (
|
CREATE TABLE room (
|
||||||
room_id TEXT NOT NULL PRIMARY KEY,
|
room_id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
room_type TEXT,
|
||||||
creation_content TEXT,
|
creation_content TEXT,
|
||||||
tombstone_content TEXT,
|
tombstone_content TEXT,
|
||||||
|
|
||||||
|
@ -17,6 +18,7 @@ CREATE TABLE room (
|
||||||
name_quality INTEGER NOT NULL DEFAULT 0,
|
name_quality INTEGER NOT NULL DEFAULT 0,
|
||||||
avatar TEXT,
|
avatar TEXT,
|
||||||
explicit_avatar INTEGER NOT NULL DEFAULT 0,
|
explicit_avatar INTEGER NOT NULL DEFAULT 0,
|
||||||
|
dm_user_id TEXT,
|
||||||
topic TEXT,
|
topic TEXT,
|
||||||
canonical_alias TEXT,
|
canonical_alias TEXT,
|
||||||
lazy_load_summary TEXT,
|
lazy_load_summary TEXT,
|
||||||
|
@ -35,11 +37,25 @@ CREATE TABLE room (
|
||||||
|
|
||||||
CONSTRAINT room_preview_event_fkey FOREIGN KEY (preview_event_rowid) REFERENCES event (rowid) ON DELETE SET NULL
|
CONSTRAINT room_preview_event_fkey FOREIGN KEY (preview_event_rowid) REFERENCES event (rowid) ON DELETE SET NULL
|
||||||
) STRICT;
|
) STRICT;
|
||||||
CREATE INDEX room_type_idx ON room (creation_content ->> 'type');
|
CREATE INDEX room_type_idx ON room (room_type);
|
||||||
CREATE INDEX room_sorting_timestamp_idx ON room (sorting_timestamp DESC);
|
CREATE INDEX room_sorting_timestamp_idx ON room (sorting_timestamp DESC);
|
||||||
|
CREATE INDEX room_preview_idx ON room (preview_event_rowid);
|
||||||
-- CREATE INDEX room_sorting_timestamp_idx ON room (unread_notifications > 0);
|
-- CREATE INDEX room_sorting_timestamp_idx ON room (unread_notifications > 0);
|
||||||
-- CREATE INDEX room_sorting_timestamp_idx ON room (unread_messages > 0);
|
-- CREATE INDEX room_sorting_timestamp_idx ON room (unread_messages > 0);
|
||||||
|
|
||||||
|
CREATE TABLE invited_room (
|
||||||
|
room_id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
received_at INTEGER NOT NULL,
|
||||||
|
invite_state TEXT NOT NULL
|
||||||
|
) STRICT;
|
||||||
|
|
||||||
|
CREATE TRIGGER invited_room_delete_on_room_insert
|
||||||
|
AFTER INSERT
|
||||||
|
ON room
|
||||||
|
BEGIN
|
||||||
|
DELETE FROM invited_room WHERE room_id = NEW.room_id;
|
||||||
|
END;
|
||||||
|
|
||||||
CREATE TABLE account_data (
|
CREATE TABLE account_data (
|
||||||
user_id TEXT NOT NULL,
|
user_id TEXT NOT NULL,
|
||||||
type TEXT NOT NULL,
|
type TEXT NOT NULL,
|
||||||
|
@ -248,7 +264,8 @@ CREATE TABLE current_state (
|
||||||
|
|
||||||
PRIMARY KEY (room_id, event_type, state_key),
|
PRIMARY KEY (room_id, event_type, state_key),
|
||||||
CONSTRAINT current_state_room_fkey FOREIGN KEY (room_id) REFERENCES room (room_id) ON DELETE CASCADE,
|
CONSTRAINT current_state_room_fkey FOREIGN KEY (room_id) REFERENCES room (room_id) ON DELETE CASCADE,
|
||||||
CONSTRAINT current_state_event_fkey FOREIGN KEY (event_rowid) REFERENCES event (rowid)
|
CONSTRAINT current_state_event_fkey FOREIGN KEY (event_rowid) REFERENCES event (rowid),
|
||||||
|
CONSTRAINT current_state_rowid_unique UNIQUE (event_rowid)
|
||||||
) STRICT, WITHOUT ROWID;
|
) STRICT, WITHOUT ROWID;
|
||||||
|
|
||||||
CREATE TABLE receipt (
|
CREATE TABLE receipt (
|
||||||
|
@ -263,3 +280,34 @@ CREATE TABLE receipt (
|
||||||
CONSTRAINT receipt_room_fkey FOREIGN KEY (room_id) REFERENCES room (room_id) ON DELETE CASCADE
|
CONSTRAINT receipt_room_fkey FOREIGN KEY (room_id) REFERENCES room (room_id) ON DELETE CASCADE
|
||||||
-- note: there's no foreign key on event ID because receipts could point at events that are too far in history.
|
-- note: there's no foreign key on event ID because receipts could point at events that are too far in history.
|
||||||
) STRICT;
|
) STRICT;
|
||||||
|
|
||||||
|
CREATE TABLE space_edge (
|
||||||
|
space_id TEXT NOT NULL,
|
||||||
|
child_id TEXT NOT NULL,
|
||||||
|
|
||||||
|
-- m.space.child fields
|
||||||
|
child_event_rowid INTEGER,
|
||||||
|
"order" TEXT NOT NULL DEFAULT '',
|
||||||
|
suggested INTEGER NOT NULL DEFAULT false CHECK ( suggested IN (false, true) ),
|
||||||
|
-- m.space.parent fields
|
||||||
|
parent_event_rowid INTEGER,
|
||||||
|
canonical INTEGER NOT NULL DEFAULT false CHECK ( canonical IN (false, true) ),
|
||||||
|
parent_validated INTEGER NOT NULL DEFAULT false CHECK ( parent_validated IN (false, true) ),
|
||||||
|
|
||||||
|
PRIMARY KEY (space_id, child_id),
|
||||||
|
CONSTRAINT space_edge_child_event_fkey FOREIGN KEY (child_event_rowid) REFERENCES event (rowid) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT space_edge_parent_event_fkey FOREIGN KEY (parent_event_rowid) REFERENCES event (rowid) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT space_edge_child_event_unique UNIQUE (child_event_rowid),
|
||||||
|
CONSTRAINT space_edge_parent_event_unique UNIQUE (parent_event_rowid)
|
||||||
|
) STRICT;
|
||||||
|
CREATE INDEX space_edge_child_idx ON space_edge (child_id);
|
||||||
|
|
||||||
|
CREATE TABLE push_registration (
|
||||||
|
device_id TEXT NOT NULL,
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
data TEXT NOT NULL,
|
||||||
|
encryption TEXT NOT NULL,
|
||||||
|
expiration INTEGER NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (device_id)
|
||||||
|
) STRICT;
|
||||||
|
|
3
pkg/hicli/database/upgrades/08-add-missing-indexes.sql
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
-- v8 (compatible with v5+): Add indexes necessary for fast room deletion
|
||||||
|
CREATE INDEX room_preview_idx ON room (preview_event_rowid);
|
||||||
|
CREATE UNIQUE INDEX current_state_rowid_unique ON current_state (event_rowid);
|
13
pkg/hicli/database/upgrades/09-invited-rooms.sql
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
-- v9 (compatible with v5+): Add table for invited rooms
|
||||||
|
CREATE TABLE invited_room (
|
||||||
|
room_id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
received_at INTEGER NOT NULL,
|
||||||
|
invite_state TEXT NOT NULL
|
||||||
|
) STRICT;
|
||||||
|
|
||||||
|
CREATE TRIGGER invited_room_delete_on_room_insert
|
||||||
|
AFTER INSERT
|
||||||
|
ON room
|
||||||
|
BEGIN
|
||||||
|
DELETE FROM invited_room WHERE room_id = NEW.room_id;
|
||||||
|
END;
|
83
pkg/hicli/database/upgrades/10-spaces.sql
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
-- v10 (compatible with v10+): Add support for spaces
|
||||||
|
ALTER TABLE room ADD COLUMN room_type TEXT;
|
||||||
|
UPDATE room SET room_type=COALESCE(creation_content->>'$.type', '');
|
||||||
|
DROP INDEX room_type_idx;
|
||||||
|
CREATE INDEX room_type_idx ON room (room_type);
|
||||||
|
|
||||||
|
CREATE TABLE space_edge (
|
||||||
|
space_id TEXT NOT NULL,
|
||||||
|
child_id TEXT NOT NULL,
|
||||||
|
|
||||||
|
-- m.space.child fields
|
||||||
|
child_event_rowid INTEGER,
|
||||||
|
"order" TEXT NOT NULL DEFAULT '',
|
||||||
|
suggested INTEGER NOT NULL DEFAULT false CHECK ( suggested IN (false, true) ),
|
||||||
|
-- m.space.parent fields
|
||||||
|
parent_event_rowid INTEGER,
|
||||||
|
canonical INTEGER NOT NULL DEFAULT false CHECK ( canonical IN (false, true) ),
|
||||||
|
parent_validated INTEGER NOT NULL DEFAULT false CHECK ( parent_validated IN (false, true) ),
|
||||||
|
|
||||||
|
PRIMARY KEY (space_id, child_id),
|
||||||
|
CONSTRAINT space_edge_child_event_fkey FOREIGN KEY (child_event_rowid) REFERENCES event (rowid) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT space_edge_parent_event_fkey FOREIGN KEY (parent_event_rowid) REFERENCES event (rowid) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT space_edge_child_event_unique UNIQUE (child_event_rowid),
|
||||||
|
CONSTRAINT space_edge_parent_event_unique UNIQUE (parent_event_rowid)
|
||||||
|
) STRICT;
|
||||||
|
CREATE INDEX space_edge_child_idx ON space_edge (child_id);
|
||||||
|
|
||||||
|
INSERT INTO space_edge (space_id, child_id, child_event_rowid, "order", suggested)
|
||||||
|
SELECT
|
||||||
|
event.room_id,
|
||||||
|
event.state_key,
|
||||||
|
event.rowid,
|
||||||
|
CASE WHEN typeof(content->>'$.order')='TEXT' THEN content->>'$.order' ELSE '' END,
|
||||||
|
CASE WHEN json_type(content, '$.suggested') IN ('true', 'false') THEN content->>'$.suggested' ELSE false END
|
||||||
|
FROM current_state
|
||||||
|
INNER JOIN event ON current_state.event_rowid = event.rowid
|
||||||
|
LEFT JOIN room ON current_state.room_id = room.room_id
|
||||||
|
WHERE type = 'm.space.child'
|
||||||
|
AND json_array_length(event.content, '$.via') > 0
|
||||||
|
AND event.state_key LIKE '!%'
|
||||||
|
AND (room.room_id IS NULL OR room.room_type = 'm.space');
|
||||||
|
|
||||||
|
INSERT INTO space_edge (space_id, child_id, parent_event_rowid, canonical)
|
||||||
|
SELECT
|
||||||
|
event.state_key,
|
||||||
|
event.room_id,
|
||||||
|
event.rowid,
|
||||||
|
CASE WHEN json_type(content, '$.canonical') IN ('true', 'false') THEN content->>'$.canonical' ELSE false END
|
||||||
|
FROM current_state
|
||||||
|
INNER JOIN event ON current_state.event_rowid = event.rowid
|
||||||
|
LEFT JOIN room ON event.state_key = room.room_id
|
||||||
|
WHERE type = 'm.space.parent'
|
||||||
|
AND json_array_length(event.content, '$.via') > 0
|
||||||
|
AND event.state_key LIKE '!%'
|
||||||
|
AND (room.room_id IS NULL OR room.room_type = 'm.space')
|
||||||
|
ON CONFLICT (space_id, child_id) DO UPDATE
|
||||||
|
SET parent_event_rowid = excluded.parent_event_rowid,
|
||||||
|
canonical = excluded.canonical;
|
||||||
|
|
||||||
|
UPDATE space_edge
|
||||||
|
SET parent_validated=(SELECT EXISTS(
|
||||||
|
SELECT 1
|
||||||
|
FROM room
|
||||||
|
INNER JOIN current_state cs ON cs.room_id = room.room_id AND cs.event_type = 'm.room.power_levels' AND cs.state_key = ''
|
||||||
|
INNER JOIN event pls ON cs.event_rowid = pls.rowid
|
||||||
|
INNER JOIN event edgeevt ON space_edge.parent_event_rowid = edgeevt.rowid
|
||||||
|
WHERE room.room_id = space_edge.space_id
|
||||||
|
AND room.room_type = 'm.space'
|
||||||
|
AND COALESCE(
|
||||||
|
(
|
||||||
|
SELECT value
|
||||||
|
FROM json_each(pls.content, '$.users')
|
||||||
|
WHERE key=edgeevt.sender AND type='integer'
|
||||||
|
),
|
||||||
|
pls.content->>'$.users_default',
|
||||||
|
0
|
||||||
|
) >= COALESCE(
|
||||||
|
pls.content->>'$.events."m.space.child"',
|
||||||
|
pls.content->>'$.state_default',
|
||||||
|
50
|
||||||
|
)
|
||||||
|
))
|
||||||
|
WHERE parent_event_rowid IS NOT NULL;
|
19
pkg/hicli/database/upgrades/11-dm-user-id.sql
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
-- v11 (compatible with v10+): Store direct chat user ID in database
|
||||||
|
ALTER TABLE room ADD COLUMN dm_user_id TEXT;
|
||||||
|
WITH dm_user_ids AS (
|
||||||
|
SELECT room_id, value
|
||||||
|
FROM room
|
||||||
|
INNER JOIN json_each(lazy_load_summary, '$."m.heroes"')
|
||||||
|
WHERE value NOT IN (SELECT value FROM json_each((
|
||||||
|
SELECT event.content
|
||||||
|
FROM current_state cs
|
||||||
|
INNER JOIN event ON cs.event_rowid = event.rowid
|
||||||
|
WHERE cs.room_id=room.room_id AND cs.event_type='io.element.functional_members' AND cs.state_key=''
|
||||||
|
), '$.service_members'))
|
||||||
|
GROUP BY room_id
|
||||||
|
HAVING COUNT(*) = 1
|
||||||
|
)
|
||||||
|
UPDATE room
|
||||||
|
SET dm_user_id=value
|
||||||
|
FROM dm_user_ids du
|
||||||
|
WHERE room.room_id=du.room_id;
|
10
pkg/hicli/database/upgrades/12-push-registrations.sql
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
-- v12 (compatible with v10+): Add table for push registrations
|
||||||
|
CREATE TABLE push_registration (
|
||||||
|
device_id TEXT NOT NULL,
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
data TEXT NOT NULL,
|
||||||
|
encryption TEXT NOT NULL,
|
||||||
|
expiration INTEGER NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (device_id)
|
||||||
|
) STRICT;
|
|
@ -59,7 +59,7 @@ func (h *HiClient) handleReceivedMegolmSession(ctx context.Context, roomID id.Ro
|
||||||
}
|
}
|
||||||
|
|
||||||
var mautrixEvt *event.Event
|
var mautrixEvt *event.Event
|
||||||
mautrixEvt, evt.Decrypted, evt.DecryptedType, err = h.decryptEvent(ctx, evt.AsRawMautrix())
|
mautrixEvt, err = h.decryptEventInto(ctx, evt.AsRawMautrix(), evt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Stringer("event_id", evt.ID).Msg("Failed to decrypt event even after receiving megolm session")
|
log.Warn().Err(err).Stringer("event_id", evt.ID).Msg("Failed to decrypt event even after receiving megolm session")
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -15,38 +15,58 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type SyncRoom struct {
|
type SyncRoom struct {
|
||||||
Meta *database.Room `json:"meta"`
|
Meta *database.Room `json:"meta"`
|
||||||
Timeline []database.TimelineRowTuple `json:"timeline"`
|
Timeline []database.TimelineRowTuple `json:"timeline"`
|
||||||
State map[event.Type]map[string]database.EventRowID `json:"state"`
|
State map[event.Type]map[string]database.EventRowID `json:"state"`
|
||||||
AccountData map[event.Type]*database.AccountData `json:"account_data"`
|
AccountData map[event.Type]*database.AccountData `json:"account_data"`
|
||||||
Events []*database.Event `json:"events"`
|
Events []*database.Event `json:"events"`
|
||||||
Reset bool `json:"reset"`
|
Reset bool `json:"reset"`
|
||||||
Notifications []SyncNotification `json:"notifications"`
|
Receipts map[id.EventID][]*database.Receipt `json:"receipts"`
|
||||||
|
|
||||||
|
DismissNotifications bool `json:"dismiss_notifications"`
|
||||||
|
Notifications []SyncNotification `json:"notifications"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SyncNotification struct {
|
type SyncNotification struct {
|
||||||
RowID database.EventRowID `json:"event_rowid"`
|
RowID database.EventRowID `json:"event_rowid"`
|
||||||
Sound bool `json:"sound"`
|
Sound bool `json:"sound"`
|
||||||
|
Highlight bool `json:"highlight"`
|
||||||
|
Event *database.Event `json:"-"`
|
||||||
|
Room *database.Room `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SyncComplete struct {
|
type SyncComplete struct {
|
||||||
Since *string `json:"since,omitempty"`
|
Since *string `json:"since,omitempty"`
|
||||||
ClearState bool `json:"clear_state,omitempty"`
|
ClearState bool `json:"clear_state,omitempty"`
|
||||||
Rooms map[id.RoomID]*SyncRoom `json:"rooms"`
|
AccountData map[event.Type]*database.AccountData `json:"account_data"`
|
||||||
AccountData map[event.Type]*database.AccountData `json:"account_data"`
|
Rooms map[id.RoomID]*SyncRoom `json:"rooms"`
|
||||||
LeftRooms []id.RoomID `json:"left_rooms"`
|
LeftRooms []id.RoomID `json:"left_rooms"`
|
||||||
|
InvitedRooms []*database.InvitedRoom `json:"invited_rooms"`
|
||||||
|
SpaceEdges map[id.RoomID][]*database.SpaceEdge `json:"space_edges"`
|
||||||
|
TopLevelSpaces []id.RoomID `json:"top_level_spaces"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SyncComplete) Notifications(yield func(SyncNotification) bool) {
|
||||||
|
for _, room := range c.Rooms {
|
||||||
|
for _, notif := range room.Notifications {
|
||||||
|
if !yield(notif) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *SyncComplete) IsEmpty() bool {
|
func (c *SyncComplete) IsEmpty() bool {
|
||||||
return len(c.Rooms) == 0 && len(c.LeftRooms) == 0 && len(c.AccountData) == 0
|
return len(c.Rooms) == 0 && len(c.LeftRooms) == 0 && len(c.InvitedRooms) == 0 && len(c.AccountData) == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
type SyncStatusType string
|
type SyncStatusType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SyncStatusOK SyncStatusType = "ok"
|
SyncStatusOK SyncStatusType = "ok"
|
||||||
SyncStatusWaiting SyncStatusType = "waiting"
|
SyncStatusWaiting SyncStatusType = "waiting"
|
||||||
SyncStatusErrored SyncStatusType = "errored"
|
SyncStatusErroring SyncStatusType = "erroring"
|
||||||
|
SyncStatusFailed SyncStatusType = "permanently-failed"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SyncStatus struct {
|
type SyncStatus struct {
|
||||||
|
|
|
@ -252,7 +252,7 @@ func (h *HiClient) Sync() {
|
||||||
log.Info().Msg("Starting syncing")
|
log.Info().Msg("Starting syncing")
|
||||||
err := h.Client.SyncWithContext(ctx)
|
err := h.Client.SyncWithContext(ctx)
|
||||||
if err != nil && ctx.Err() == nil {
|
if err != nil && ctx.Err() == nil {
|
||||||
h.markSyncErrored(err)
|
h.markSyncErrored(err, true)
|
||||||
log.Err(err).Msg("Fatal error in syncer")
|
log.Err(err).Msg("Fatal error in syncer")
|
||||||
} else {
|
} else {
|
||||||
h.SyncStatus.Store(syncWaiting)
|
h.SyncStatus.Store(syncWaiting)
|
||||||
|
|
|
@ -14,11 +14,9 @@ import (
|
||||||
|
|
||||||
func (h *HiClient) getInitialSyncRoom(ctx context.Context, room *database.Room) *SyncRoom {
|
func (h *HiClient) getInitialSyncRoom(ctx context.Context, room *database.Room) *SyncRoom {
|
||||||
syncRoom := &SyncRoom{
|
syncRoom := &SyncRoom{
|
||||||
Meta: room,
|
Meta: room,
|
||||||
Events: make([]*database.Event, 0, 2),
|
Events: make([]*database.Event, 0, 2),
|
||||||
Timeline: make([]database.TimelineRowTuple, 0),
|
State: map[event.Type]map[string]database.EventRowID{},
|
||||||
State: map[event.Type]map[string]database.EventRowID{},
|
|
||||||
Notifications: make([]SyncNotification, 0),
|
|
||||||
}
|
}
|
||||||
ad, err := h.DB.AccountData.GetAllRoom(ctx, h.Account.UserID, room.ID)
|
ad, err := h.DB.AccountData.GetAllRoom(ctx, h.Account.UserID, room.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -26,7 +24,6 @@ func (h *HiClient) getInitialSyncRoom(ctx context.Context, room *database.Room)
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
syncRoom.AccountData = make(map[event.Type]*database.AccountData)
|
|
||||||
} else {
|
} else {
|
||||||
syncRoom.AccountData = make(map[event.Type]*database.AccountData, len(ad))
|
syncRoom.AccountData = make(map[event.Type]*database.AccountData, len(ad))
|
||||||
for _, data := range ad {
|
for _, data := range ad {
|
||||||
|
@ -69,6 +66,49 @@ func (h *HiClient) getInitialSyncRoom(ctx context.Context, room *database.Room)
|
||||||
func (h *HiClient) GetInitialSync(ctx context.Context, batchSize int) iter.Seq[*SyncComplete] {
|
func (h *HiClient) GetInitialSync(ctx context.Context, batchSize int) iter.Seq[*SyncComplete] {
|
||||||
return func(yield func(*SyncComplete) bool) {
|
return func(yield func(*SyncComplete) bool) {
|
||||||
maxTS := time.Now().Add(1 * time.Hour)
|
maxTS := time.Now().Add(1 * time.Hour)
|
||||||
|
{
|
||||||
|
spaces, err := h.DB.Room.GetAllSpaces(ctx)
|
||||||
|
if err != nil {
|
||||||
|
if ctx.Err() == nil {
|
||||||
|
zerolog.Ctx(ctx).Err(err).Msg("Failed to get initial spaces to send to client")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
payload := SyncComplete{
|
||||||
|
Rooms: make(map[id.RoomID]*SyncRoom, len(spaces)),
|
||||||
|
}
|
||||||
|
for _, room := range spaces {
|
||||||
|
payload.Rooms[room.ID] = h.getInitialSyncRoom(ctx, room)
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
payload.TopLevelSpaces, err = h.DB.SpaceEdge.GetTopLevelIDs(ctx, h.Account.UserID)
|
||||||
|
if err != nil {
|
||||||
|
if ctx.Err() == nil {
|
||||||
|
zerolog.Ctx(ctx).Err(err).Msg("Failed to get top-level space IDs to send to client")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
payload.SpaceEdges, err = h.DB.SpaceEdge.GetAll(ctx, "")
|
||||||
|
if err != nil {
|
||||||
|
if ctx.Err() == nil {
|
||||||
|
zerolog.Ctx(ctx).Err(err).Msg("Failed to get space edges to send to client")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
payload.InvitedRooms, err = h.DB.InvitedRoom.GetAll(ctx)
|
||||||
|
if err != nil {
|
||||||
|
if ctx.Err() == nil {
|
||||||
|
zerolog.Ctx(ctx).Err(err).Msg("Failed to get invited rooms to send to client")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
payload.ClearState = true
|
||||||
|
if !yield(&payload) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
for i := 0; ; i++ {
|
for i := 0; ; i++ {
|
||||||
rooms, err := h.DB.Room.GetBySortTS(ctx, maxTS, batchSize)
|
rooms, err := h.DB.Room.GetBySortTS(ctx, maxTS, batchSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -78,12 +118,7 @@ func (h *HiClient) GetInitialSync(ctx context.Context, batchSize int) iter.Seq[*
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
payload := SyncComplete{
|
payload := SyncComplete{
|
||||||
Rooms: make(map[id.RoomID]*SyncRoom, len(rooms)-1),
|
Rooms: make(map[id.RoomID]*SyncRoom, len(rooms)),
|
||||||
LeftRooms: make([]id.RoomID, 0),
|
|
||||||
AccountData: make(map[event.Type]*database.AccountData),
|
|
||||||
}
|
|
||||||
if i == 0 {
|
|
||||||
payload.ClearState = true
|
|
||||||
}
|
}
|
||||||
for _, room := range rooms {
|
for _, room := range rooms {
|
||||||
if room.SortingTimestamp == rooms[len(rooms)-1].SortingTimestamp {
|
if room.SortingTimestamp == rooms[len(rooms)-1].SortingTimestamp {
|
||||||
|
@ -95,7 +130,9 @@ func (h *HiClient) GetInitialSync(ctx context.Context, batchSize int) iter.Seq[*
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !yield(&payload) || len(rooms) < batchSize {
|
if !yield(&payload) {
|
||||||
|
return
|
||||||
|
} else if len(rooms) < batchSize {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -106,8 +143,6 @@ func (h *HiClient) GetInitialSync(ctx context.Context, batchSize int) iter.Seq[*
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
payload := SyncComplete{
|
payload := SyncComplete{
|
||||||
Rooms: make(map[id.RoomID]*SyncRoom, 0),
|
|
||||||
LeftRooms: make([]id.RoomID, 0),
|
|
||||||
AccountData: make(map[event.Type]*database.AccountData, len(ad)),
|
AccountData: make(map[event.Type]*database.AccountData, len(ad)),
|
||||||
}
|
}
|
||||||
for _, data := range ad {
|
for _, data := range ad {
|
||||||
|
|
|
@ -86,10 +86,22 @@ func (h *HiClient) handleJSONCommand(ctx context.Context, req *JSONCommand) (any
|
||||||
return unmarshalAndCall(req.Data, func(params *getProfileParams) (*mautrix.RespUserProfile, error) {
|
return unmarshalAndCall(req.Data, func(params *getProfileParams) (*mautrix.RespUserProfile, error) {
|
||||||
return h.Client.GetProfile(ctx, params.UserID)
|
return h.Client.GetProfile(ctx, params.UserID)
|
||||||
})
|
})
|
||||||
|
case "set_profile_field":
|
||||||
|
return unmarshalAndCall(req.Data, func(params *setProfileFieldParams) (bool, error) {
|
||||||
|
return true, h.Client.UnstableSetProfileField(ctx, params.Field, params.Value)
|
||||||
|
})
|
||||||
case "get_mutual_rooms":
|
case "get_mutual_rooms":
|
||||||
return unmarshalAndCall(req.Data, func(params *getProfileParams) ([]id.RoomID, error) {
|
return unmarshalAndCall(req.Data, func(params *getProfileParams) ([]id.RoomID, error) {
|
||||||
return h.GetMutualRooms(ctx, params.UserID)
|
return h.GetMutualRooms(ctx, params.UserID)
|
||||||
})
|
})
|
||||||
|
case "track_user_devices":
|
||||||
|
return unmarshalAndCall(req.Data, func(params *getProfileParams) (*ProfileEncryptionInfo, error) {
|
||||||
|
err := h.TrackUserDevices(ctx, params.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return h.GetProfileEncryptionInfo(ctx, params.UserID)
|
||||||
|
})
|
||||||
case "get_profile_encryption_info":
|
case "get_profile_encryption_info":
|
||||||
return unmarshalAndCall(req.Data, func(params *getProfileParams) (*ProfileEncryptionInfo, error) {
|
return unmarshalAndCall(req.Data, func(params *getProfileParams) (*ProfileEncryptionInfo, error) {
|
||||||
return h.GetProfileEncryptionInfo(ctx, params.UserID)
|
return h.GetProfileEncryptionInfo(ctx, params.UserID)
|
||||||
|
@ -98,10 +110,10 @@ func (h *HiClient) handleJSONCommand(ctx context.Context, req *JSONCommand) (any
|
||||||
return unmarshalAndCall(req.Data, func(params *getEventParams) (*database.Event, error) {
|
return unmarshalAndCall(req.Data, func(params *getEventParams) (*database.Event, error) {
|
||||||
return h.GetEvent(ctx, params.RoomID, params.EventID)
|
return h.GetEvent(ctx, params.RoomID, params.EventID)
|
||||||
})
|
})
|
||||||
case "get_events_by_rowids":
|
//case "get_events_by_rowids":
|
||||||
return unmarshalAndCall(req.Data, func(params *getEventsByRowIDsParams) ([]*database.Event, error) {
|
// return unmarshalAndCall(req.Data, func(params *getEventsByRowIDsParams) ([]*database.Event, error) {
|
||||||
return h.GetEventsByRowIDs(ctx, params.RowIDs)
|
// return h.GetEventsByRowIDs(ctx, params.RowIDs)
|
||||||
})
|
// })
|
||||||
case "get_room_state":
|
case "get_room_state":
|
||||||
return unmarshalAndCall(req.Data, func(params *getRoomStateParams) ([]*database.Event, error) {
|
return unmarshalAndCall(req.Data, func(params *getRoomStateParams) ([]*database.Event, error) {
|
||||||
return h.GetRoomState(ctx, params.RoomID, params.IncludeMembers, params.FetchMembers, params.Refetch)
|
return h.GetRoomState(ctx, params.RoomID, params.IncludeMembers, params.FetchMembers, params.Refetch)
|
||||||
|
@ -110,6 +122,10 @@ func (h *HiClient) handleJSONCommand(ctx context.Context, req *JSONCommand) (any
|
||||||
return unmarshalAndCall(req.Data, func(params *getSpecificRoomStateParams) ([]*database.Event, error) {
|
return unmarshalAndCall(req.Data, func(params *getSpecificRoomStateParams) ([]*database.Event, error) {
|
||||||
return h.DB.CurrentState.GetMany(ctx, params.Keys)
|
return h.DB.CurrentState.GetMany(ctx, params.Keys)
|
||||||
})
|
})
|
||||||
|
case "get_receipts":
|
||||||
|
return unmarshalAndCall(req.Data, func(params *getReceiptsParams) (map[id.EventID][]*database.Receipt, error) {
|
||||||
|
return h.GetReceipts(ctx, params.RoomID, params.EventIDs)
|
||||||
|
})
|
||||||
case "paginate":
|
case "paginate":
|
||||||
return unmarshalAndCall(req.Data, func(params *paginateParams) (*PaginationResponse, error) {
|
return unmarshalAndCall(req.Data, func(params *paginateParams) (*PaginationResponse, error) {
|
||||||
return h.Paginate(ctx, params.RoomID, params.MaxTimelineID, params.Limit)
|
return h.Paginate(ctx, params.RoomID, params.MaxTimelineID, params.Limit)
|
||||||
|
@ -118,6 +134,21 @@ func (h *HiClient) handleJSONCommand(ctx context.Context, req *JSONCommand) (any
|
||||||
return unmarshalAndCall(req.Data, func(params *paginateParams) (*PaginationResponse, error) {
|
return unmarshalAndCall(req.Data, func(params *paginateParams) (*PaginationResponse, error) {
|
||||||
return h.PaginateServer(ctx, params.RoomID, params.Limit)
|
return h.PaginateServer(ctx, params.RoomID, params.Limit)
|
||||||
})
|
})
|
||||||
|
case "get_room_summary":
|
||||||
|
return unmarshalAndCall(req.Data, func(params *joinRoomParams) (*mautrix.RespRoomSummary, error) {
|
||||||
|
return h.Client.GetRoomSummary(ctx, params.RoomIDOrAlias, params.Via...)
|
||||||
|
})
|
||||||
|
case "join_room":
|
||||||
|
return unmarshalAndCall(req.Data, func(params *joinRoomParams) (*mautrix.RespJoinRoom, error) {
|
||||||
|
return h.Client.JoinRoom(ctx, params.RoomIDOrAlias, &mautrix.ReqJoinRoom{
|
||||||
|
Via: params.Via,
|
||||||
|
Reason: params.Reason,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
case "leave_room":
|
||||||
|
return unmarshalAndCall(req.Data, func(params *leaveRoomParams) (*mautrix.RespLeaveRoom, error) {
|
||||||
|
return h.Client.LeaveRoom(ctx, params.RoomID, &mautrix.ReqLeave{Reason: params.Reason})
|
||||||
|
})
|
||||||
case "ensure_group_session_shared":
|
case "ensure_group_session_shared":
|
||||||
return unmarshalAndCall(req.Data, func(params *ensureGroupSessionSharedParams) (bool, error) {
|
return unmarshalAndCall(req.Data, func(params *ensureGroupSessionSharedParams) (bool, error) {
|
||||||
return true, h.EnsureGroupSessionShared(ctx, params.RoomID)
|
return true, h.EnsureGroupSessionShared(ctx, params.RoomID)
|
||||||
|
@ -126,6 +157,8 @@ func (h *HiClient) handleJSONCommand(ctx context.Context, req *JSONCommand) (any
|
||||||
return unmarshalAndCall(req.Data, func(params *resolveAliasParams) (*mautrix.RespAliasResolve, error) {
|
return unmarshalAndCall(req.Data, func(params *resolveAliasParams) (*mautrix.RespAliasResolve, error) {
|
||||||
return h.Client.ResolveAlias(ctx, params.Alias)
|
return h.Client.ResolveAlias(ctx, params.Alias)
|
||||||
})
|
})
|
||||||
|
case "request_openid_token":
|
||||||
|
return h.Client.RequestOpenIDToken(ctx)
|
||||||
case "logout":
|
case "logout":
|
||||||
if h.LogoutFunc == nil {
|
if h.LogoutFunc == nil {
|
||||||
return nil, errors.New("logout not supported")
|
return nil, errors.New("logout not supported")
|
||||||
|
@ -168,6 +201,10 @@ func (h *HiClient) handleJSONCommand(ctx context.Context, req *JSONCommand) (any
|
||||||
}
|
}
|
||||||
return cli.GetLoginFlows(ctx)
|
return cli.GetLoginFlows(ctx)
|
||||||
})
|
})
|
||||||
|
case "register_push":
|
||||||
|
return unmarshalAndCall(req.Data, func(params *database.PushRegistration) (bool, error) {
|
||||||
|
return true, h.DB.PushRegistration.Put(ctx, params)
|
||||||
|
})
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unknown command %q", req.Command)
|
return nil, fmt.Errorf("unknown command %q", req.Command)
|
||||||
}
|
}
|
||||||
|
@ -246,14 +283,19 @@ type getProfileParams struct {
|
||||||
UserID id.UserID `json:"user_id"`
|
UserID id.UserID `json:"user_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type setProfileFieldParams struct {
|
||||||
|
Field string `json:"field"`
|
||||||
|
Value any `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
type getEventParams struct {
|
type getEventParams struct {
|
||||||
RoomID id.RoomID `json:"room_id"`
|
RoomID id.RoomID `json:"room_id"`
|
||||||
EventID id.EventID `json:"event_id"`
|
EventID id.EventID `json:"event_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type getEventsByRowIDsParams struct {
|
//type getEventsByRowIDsParams struct {
|
||||||
RowIDs []database.EventRowID `json:"row_ids"`
|
// RowIDs []database.EventRowID `json:"row_ids"`
|
||||||
}
|
//}
|
||||||
|
|
||||||
type getRoomStateParams struct {
|
type getRoomStateParams struct {
|
||||||
RoomID id.RoomID `json:"room_id"`
|
RoomID id.RoomID `json:"room_id"`
|
||||||
|
@ -302,3 +344,19 @@ type paginateParams struct {
|
||||||
MaxTimelineID database.TimelineRowID `json:"max_timeline_id"`
|
MaxTimelineID database.TimelineRowID `json:"max_timeline_id"`
|
||||||
Limit int `json:"limit"`
|
Limit int `json:"limit"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type joinRoomParams struct {
|
||||||
|
RoomIDOrAlias string `json:"room_id_or_alias"`
|
||||||
|
Via []string `json:"via"`
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type leaveRoomParams struct {
|
||||||
|
RoomID id.RoomID `json:"room_id"`
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type getReceiptsParams struct {
|
||||||
|
RoomID id.RoomID `json:"room_id"`
|
||||||
|
EventIDs []id.EventID `json:"event_ids"`
|
||||||
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ import (
|
||||||
|
|
||||||
var ErrPaginationAlreadyInProgress = errors.New("pagination is already in progress")
|
var ErrPaginationAlreadyInProgress = errors.New("pagination is already in progress")
|
||||||
|
|
||||||
func (h *HiClient) GetEventsByRowIDs(ctx context.Context, rowIDs []database.EventRowID) ([]*database.Event, error) {
|
/*func (h *HiClient) GetEventsByRowIDs(ctx context.Context, rowIDs []database.EventRowID) ([]*database.Event, error) {
|
||||||
events, err := h.DB.Event.GetByRowIDs(ctx, rowIDs...)
|
events, err := h.DB.Event.GetByRowIDs(ctx, rowIDs...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -51,7 +51,7 @@ func (h *HiClient) GetEventsByRowIDs(ctx context.Context, rowIDs []database.Even
|
||||||
// TODO slow path where events are collected and filling is done one room at a time?
|
// TODO slow path where events are collected and filling is done one room at a time?
|
||||||
}
|
}
|
||||||
return events, nil
|
return events, nil
|
||||||
}
|
}*/
|
||||||
|
|
||||||
func (h *HiClient) GetEvent(ctx context.Context, roomID id.RoomID, eventID id.EventID) (*database.Event, error) {
|
func (h *HiClient) GetEvent(ctx context.Context, roomID id.RoomID, eventID id.EventID) (*database.Event, error) {
|
||||||
if evt, err := h.DB.Event.GetByID(ctx, eventID); err != nil {
|
if evt, err := h.DB.Event.GetByID(ctx, eventID); err != nil {
|
||||||
|
@ -121,13 +121,14 @@ func (h *HiClient) processGetRoomState(ctx context.Context, roomID id.RoomID, fe
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to save events: %w", err)
|
return fmt.Errorf("failed to save events: %w", err)
|
||||||
}
|
}
|
||||||
|
sdc := &spaceDataCollector{}
|
||||||
for i := range currentStateEntries {
|
for i := range currentStateEntries {
|
||||||
currentStateEntries[i].EventRowID = dbEvts[i].RowID
|
currentStateEntries[i].EventRowID = dbEvts[i].RowID
|
||||||
if mediaReferenceEntries[i] != nil {
|
if mediaReferenceEntries[i] != nil {
|
||||||
mediaReferenceEntries[i].EventRowID = dbEvts[i].RowID
|
mediaReferenceEntries[i].EventRowID = dbEvts[i].RowID
|
||||||
}
|
}
|
||||||
if evts[i].Type != event.StateMember {
|
if evts[i].Type != event.StateMember {
|
||||||
processImportantEvent(ctx, evts[i], room, updatedRoom)
|
processImportantEvent(ctx, evts[i], room, updatedRoom, dbEvts[i].RowID, sdc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err = h.DB.Media.AddMany(ctx, mediaCacheEntries)
|
err = h.DB.Media.AddMany(ctx, mediaCacheEntries)
|
||||||
|
@ -146,6 +147,11 @@ func (h *HiClient) processGetRoomState(ctx context.Context, roomID id.RoomID, fe
|
||||||
return fmt.Errorf("failed to save current state entries: %w", err)
|
return fmt.Errorf("failed to save current state entries: %w", err)
|
||||||
}
|
}
|
||||||
roomChanged := updatedRoom.CheckChangesAndCopyInto(room)
|
roomChanged := updatedRoom.CheckChangesAndCopyInto(room)
|
||||||
|
// TODO dispatch space edge changes if something changed? (fairly unlikely though)
|
||||||
|
err = sdc.Apply(ctx, room, h.DB.SpaceEdge)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if roomChanged {
|
if roomChanged {
|
||||||
err = h.DB.Room.Upsert(ctx, updatedRoom)
|
err = h.DB.Room.Upsert(ctx, updatedRoom)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -155,17 +161,9 @@ func (h *HiClient) processGetRoomState(ctx context.Context, roomID id.RoomID, fe
|
||||||
h.EventHandler(&SyncComplete{
|
h.EventHandler(&SyncComplete{
|
||||||
Rooms: map[id.RoomID]*SyncRoom{
|
Rooms: map[id.RoomID]*SyncRoom{
|
||||||
roomID: {
|
roomID: {
|
||||||
Meta: room,
|
Meta: room,
|
||||||
Timeline: make([]database.TimelineRowTuple, 0),
|
|
||||||
State: make(map[event.Type]map[string]database.EventRowID),
|
|
||||||
AccountData: make(map[event.Type]*database.AccountData),
|
|
||||||
Events: make([]*database.Event, 0),
|
|
||||||
Reset: false,
|
|
||||||
Notifications: make([]SyncNotification, 0),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
AccountData: make(map[event.Type]*database.AccountData),
|
|
||||||
LeftRooms: make([]id.RoomID, 0),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -196,22 +194,87 @@ func (h *HiClient) GetRoomState(ctx context.Context, roomID id.RoomID, includeMe
|
||||||
}
|
}
|
||||||
|
|
||||||
type PaginationResponse struct {
|
type PaginationResponse struct {
|
||||||
Events []*database.Event `json:"events"`
|
Events []*database.Event `json:"events"`
|
||||||
HasMore bool `json:"has_more"`
|
Receipts map[id.EventID][]*database.Receipt `json:"receipts"`
|
||||||
|
RelatedEvents []*database.Event `json:"related_events"`
|
||||||
|
HasMore bool `json:"has_more"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HiClient) Paginate(ctx context.Context, roomID id.RoomID, maxTimelineID database.TimelineRowID, limit int) (*PaginationResponse, error) {
|
func (h *HiClient) Paginate(ctx context.Context, roomID id.RoomID, maxTimelineID database.TimelineRowID, limit int) (*PaginationResponse, error) {
|
||||||
evts, err := h.DB.Timeline.Get(ctx, roomID, limit, maxTimelineID)
|
evts, err := h.DB.Timeline.Get(ctx, roomID, limit, maxTimelineID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if len(evts) > 0 {
|
}
|
||||||
|
var resp *PaginationResponse
|
||||||
|
if len(evts) > 0 {
|
||||||
for _, evt := range evts {
|
for _, evt := range evts {
|
||||||
h.ReprocessExistingEvent(ctx, evt)
|
h.ReprocessExistingEvent(ctx, evt)
|
||||||
}
|
}
|
||||||
return &PaginationResponse{Events: evts, HasMore: true}, nil
|
resp = &PaginationResponse{Events: evts, HasMore: true}
|
||||||
} else {
|
} else {
|
||||||
return h.PaginateServer(ctx, roomID, limit)
|
resp, err = h.PaginateServer(ctx, roomID, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
resp.RelatedEvents = make([]*database.Event, 0)
|
||||||
|
eventIDs := make([]id.EventID, len(resp.Events))
|
||||||
|
eventMap := make(map[id.EventID]struct{})
|
||||||
|
for i := len(resp.Events) - 1; i >= 0; i-- {
|
||||||
|
evt := resp.Events[i]
|
||||||
|
eventIDs[i] = evt.ID
|
||||||
|
eventMap[evt.ID] = struct{}{}
|
||||||
|
replyTo := evt.GetReplyTo()
|
||||||
|
if replyTo != "" {
|
||||||
|
_, replyToAdded := eventMap[replyTo]
|
||||||
|
if !replyToAdded {
|
||||||
|
dbEvt, err := h.DB.Event.GetByID(ctx, replyTo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get reply-to event: %w", err)
|
||||||
|
} else if dbEvt != nil {
|
||||||
|
resp.RelatedEvents = append(resp.RelatedEvents, dbEvt)
|
||||||
|
eventMap[replyTo] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp.Receipts, err = h.GetReceipts(ctx, roomID, eventIDs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get receipts: %w", err)
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HiClient) GetReceipts(ctx context.Context, roomID id.RoomID, eventIDs []id.EventID) (map[id.EventID][]*database.Receipt, error) {
|
||||||
|
receipts, err := h.DB.Receipt.GetManyRead(ctx, roomID, eventIDs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
encounteredUsers := map[id.UserID]struct{}{
|
||||||
|
// Never include own receipts
|
||||||
|
h.Account.UserID: {},
|
||||||
|
}
|
||||||
|
// If there are multiple receipts (e.g. due to threads), only keep the one for the latest event (first in the array)
|
||||||
|
// The input event IDs are already sorted in reverse chronological order
|
||||||
|
for _, evtID := range eventIDs {
|
||||||
|
receiptArr := receipts[evtID]
|
||||||
|
i := 0
|
||||||
|
for _, receipt := range receiptArr {
|
||||||
|
_, alreadyEncountered := encounteredUsers[receipt.UserID]
|
||||||
|
if alreadyEncountered {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Clear room ID for efficiency
|
||||||
|
receipt.RoomID = ""
|
||||||
|
encounteredUsers[receipt.UserID] = struct{}{}
|
||||||
|
receiptArr[i] = receipt
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
if len(receiptArr) > 0 && i < len(receiptArr) {
|
||||||
|
receipts[evtID] = receiptArr[:i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return receipts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HiClient) PaginateServer(ctx context.Context, roomID id.RoomID, limit int) (*PaginationResponse, error) {
|
func (h *HiClient) PaginateServer(ctx context.Context, roomID id.RoomID, limit int) (*PaginationResponse, error) {
|
||||||
|
|
|
@ -91,3 +91,8 @@ func (h *HiClient) GetProfileEncryptionInfo(ctx context.Context, userID id.UserI
|
||||||
}
|
}
|
||||||
return &resp, nil
|
return &resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *HiClient) TrackUserDevices(ctx context.Context, userID id.UserID) error {
|
||||||
|
_, err := h.Crypto.FetchKeys(ctx, []id.UserID{userID}, true)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
|
@ -71,6 +71,11 @@ func (h *HiClient) SendMessage(
|
||||||
relatesTo *event.RelatesTo,
|
relatesTo *event.RelatesTo,
|
||||||
mentions *event.Mentions,
|
mentions *event.Mentions,
|
||||||
) (*database.Event, error) {
|
) (*database.Event, error) {
|
||||||
|
var unencrypted bool
|
||||||
|
if strings.HasPrefix(text, "/unencrypted ") {
|
||||||
|
text = strings.TrimPrefix(text, "/unencrypted ")
|
||||||
|
unencrypted = true
|
||||||
|
}
|
||||||
if strings.HasPrefix(text, "/raw ") {
|
if strings.HasPrefix(text, "/raw ") {
|
||||||
parts := strings.SplitN(text, " ", 3)
|
parts := strings.SplitN(text, " ", 3)
|
||||||
if len(parts) < 2 || len(parts[1]) == 0 {
|
if len(parts) < 2 || len(parts[1]) == 0 {
|
||||||
|
@ -85,7 +90,18 @@ func (h *HiClient) SendMessage(
|
||||||
if !json.Valid(content) {
|
if !json.Valid(content) {
|
||||||
return nil, fmt.Errorf("invalid JSON in /raw command")
|
return nil, fmt.Errorf("invalid JSON in /raw command")
|
||||||
}
|
}
|
||||||
return h.send(ctx, roomID, event.Type{Type: parts[1]}, content, "")
|
return h.send(ctx, roomID, event.Type{Type: parts[1]}, content, "", unencrypted)
|
||||||
|
} else if strings.HasPrefix(text, "/rawstate ") {
|
||||||
|
parts := strings.SplitN(text, " ", 4)
|
||||||
|
if len(parts) < 4 || len(parts[1]) == 0 {
|
||||||
|
return nil, fmt.Errorf("invalid /rawstate command")
|
||||||
|
}
|
||||||
|
content := json.RawMessage(parts[3])
|
||||||
|
if !json.Valid(content) {
|
||||||
|
return nil, fmt.Errorf("invalid JSON in /rawstate command")
|
||||||
|
}
|
||||||
|
_, err := h.SetState(ctx, roomID, event.Type{Type: parts[1], Class: event.StateEventType}, parts[2], content)
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
var content event.MessageEventContent
|
var content event.MessageEventContent
|
||||||
msgType := event.MsgText
|
msgType := event.MsgText
|
||||||
|
@ -148,7 +164,12 @@ func (h *HiClient) SendMessage(
|
||||||
content.RelatesTo = relatesTo
|
content.RelatesTo = relatesTo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return h.send(ctx, roomID, event.EventMessage, &event.Content{Parsed: content, Raw: extra}, origText)
|
evtType := event.EventMessage
|
||||||
|
if content.MsgType == "m.sticker" {
|
||||||
|
content.MsgType = ""
|
||||||
|
evtType = event.EventSticker
|
||||||
|
}
|
||||||
|
return h.send(ctx, roomID, evtType, &event.Content{Parsed: content, Raw: extra}, origText, unencrypted)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HiClient) MarkRead(ctx context.Context, roomID id.RoomID, eventID id.EventID, receiptType event.ReceiptType) error {
|
func (h *HiClient) MarkRead(ctx context.Context, roomID id.RoomID, eventID id.EventID, receiptType event.ReceiptType) error {
|
||||||
|
@ -212,7 +233,7 @@ func (h *HiClient) Send(
|
||||||
evtType event.Type,
|
evtType event.Type,
|
||||||
content any,
|
content any,
|
||||||
) (*database.Event, error) {
|
) (*database.Event, error) {
|
||||||
return h.send(ctx, roomID, evtType, content, "")
|
return h.send(ctx, roomID, evtType, content, "", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HiClient) Resend(ctx context.Context, txnID string) (*database.Event, error) {
|
func (h *HiClient) Resend(ctx context.Context, txnID string) (*database.Event, error) {
|
||||||
|
@ -241,6 +262,7 @@ func (h *HiClient) send(
|
||||||
evtType event.Type,
|
evtType event.Type,
|
||||||
content any,
|
content any,
|
||||||
overrideEditSource string,
|
overrideEditSource string,
|
||||||
|
disableEncryption bool,
|
||||||
) (*database.Event, error) {
|
) (*database.Event, error) {
|
||||||
room, err := h.DB.Room.Get(ctx, roomID)
|
room, err := h.DB.Room.Get(ctx, roomID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -261,7 +283,7 @@ func (h *HiClient) send(
|
||||||
Reactions: map[string]int{},
|
Reactions: map[string]int{},
|
||||||
LastEditRowID: ptr.Ptr(database.EventRowID(0)),
|
LastEditRowID: ptr.Ptr(database.EventRowID(0)),
|
||||||
}
|
}
|
||||||
if room.EncryptionEvent != nil && evtType != event.EventReaction {
|
if room.EncryptionEvent != nil && evtType != event.EventReaction && !disableEncryption {
|
||||||
dbEvt.Type = event.EventEncrypted.Type
|
dbEvt.Type = event.EventEncrypted.Type
|
||||||
dbEvt.DecryptedType = evtType.Type
|
dbEvt.DecryptedType = evtType.Type
|
||||||
dbEvt.Decrypted, err = json.Marshal(content)
|
dbEvt.Decrypted, err = json.Marshal(content)
|
||||||
|
@ -281,7 +303,7 @@ func (h *HiClient) send(
|
||||||
var inlineImages []id.ContentURI
|
var inlineImages []id.ContentURI
|
||||||
mautrixEvt := dbEvt.AsRawMautrix()
|
mautrixEvt := dbEvt.AsRawMautrix()
|
||||||
dbEvt.LocalContent, inlineImages = h.calculateLocalContent(ctx, dbEvt, mautrixEvt)
|
dbEvt.LocalContent, inlineImages = h.calculateLocalContent(ctx, dbEvt, mautrixEvt)
|
||||||
if overrideEditSource != "" {
|
if overrideEditSource != "" && dbEvt.LocalContent != nil {
|
||||||
dbEvt.LocalContent.EditSource = overrideEditSource
|
dbEvt.LocalContent.EditSource = overrideEditSource
|
||||||
}
|
}
|
||||||
_, err = h.DB.Event.Insert(ctx, dbEvt)
|
_, err = h.DB.Event.Insert(ctx, dbEvt)
|
||||||
|
|
|
@ -39,13 +39,16 @@ type syncContext struct {
|
||||||
evt *SyncComplete
|
evt *SyncComplete
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HiClient) markSyncErrored(err error) {
|
func (h *HiClient) markSyncErrored(err error, permanent bool) {
|
||||||
stat := &SyncStatus{
|
stat := &SyncStatus{
|
||||||
Type: SyncStatusErrored,
|
Type: SyncStatusErroring,
|
||||||
Error: err.Error(),
|
Error: err.Error(),
|
||||||
ErrorCount: h.syncErrors,
|
ErrorCount: h.syncErrors,
|
||||||
LastSync: jsontime.UM(h.lastSync),
|
LastSync: jsontime.UM(h.lastSync),
|
||||||
}
|
}
|
||||||
|
if permanent {
|
||||||
|
stat.Type = SyncStatusFailed
|
||||||
|
}
|
||||||
h.SyncStatus.Store(stat)
|
h.SyncStatus.Store(stat)
|
||||||
h.EventHandler(stat)
|
h.EventHandler(stat)
|
||||||
}
|
}
|
||||||
|
@ -85,6 +88,7 @@ func (h *HiClient) preProcessSyncResponse(ctx context.Context, resp *mautrix.Res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resp.ToDevice.Events = postponedToDevices
|
resp.ToDevice.Events = postponedToDevices
|
||||||
|
h.Crypto.MarkOlmHashSavePoint(ctx)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -148,14 +152,20 @@ func (h *HiClient) processSyncResponse(ctx context.Context, resp *mautrix.RespSy
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx.Value(syncContextKey).(*syncContext).evt.AccountData = accountData
|
ctx.Value(syncContextKey).(*syncContext).evt.AccountData = accountData
|
||||||
|
for roomID, room := range resp.Rooms.Invite {
|
||||||
|
err = h.processSyncInvitedRoom(ctx, roomID, room)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to process invited room %s: %w", roomID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
for roomID, room := range resp.Rooms.Join {
|
for roomID, room := range resp.Rooms.Join {
|
||||||
err := h.processSyncJoinedRoom(ctx, roomID, room)
|
err = h.processSyncJoinedRoom(ctx, roomID, room)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to process joined room %s: %w", roomID, err)
|
return fmt.Errorf("failed to process joined room %s: %w", roomID, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for roomID, room := range resp.Rooms.Leave {
|
for roomID, room := range resp.Rooms.Leave {
|
||||||
err := h.processSyncLeftRoom(ctx, roomID, room)
|
err = h.processSyncLeftRoom(ctx, roomID, room)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to process left room %s: %w", roomID, err)
|
return fmt.Errorf("failed to process left room %s: %w", roomID, err)
|
||||||
}
|
}
|
||||||
|
@ -177,6 +187,9 @@ func (h *HiClient) receiptsToList(content *event.ReceiptEventContent) ([]*databa
|
||||||
if userID == h.Account.UserID {
|
if userID == h.Account.UserID {
|
||||||
newOwnReceipts = append(newOwnReceipts, eventID)
|
newOwnReceipts = append(newOwnReceipts, eventID)
|
||||||
}
|
}
|
||||||
|
if receiptInfo.ThreadID == event.ReadReceiptThreadMain {
|
||||||
|
receiptInfo.ThreadID = ""
|
||||||
|
}
|
||||||
receiptList = append(receiptList, &database.Receipt{
|
receiptList = append(receiptList, &database.Receipt{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
ReceiptType: receiptType,
|
ReceiptType: receiptType,
|
||||||
|
@ -190,6 +203,27 @@ func (h *HiClient) receiptsToList(content *event.ReceiptEventContent) ([]*databa
|
||||||
return receiptList, newOwnReceipts
|
return receiptList, newOwnReceipts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *HiClient) processSyncInvitedRoom(ctx context.Context, roomID id.RoomID, room *mautrix.SyncInvitedRoom) error {
|
||||||
|
ir := &database.InvitedRoom{
|
||||||
|
ID: roomID,
|
||||||
|
CreatedAt: jsontime.UnixMilliNow(),
|
||||||
|
InviteState: room.State.Events,
|
||||||
|
}
|
||||||
|
for _, evt := range room.State.Events {
|
||||||
|
if evt.Type == event.StateMember && evt.GetStateKey() == h.Account.UserID.String() && evt.Timestamp != 0 {
|
||||||
|
ir.CreatedAt = jsontime.UM(time.UnixMilli(evt.Timestamp))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err := h.DB.InvitedRoom.Upsert(ctx, ir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to save invited room: %w", err)
|
||||||
|
}
|
||||||
|
syncEvt := ctx.Value(syncContextKey).(*syncContext).evt
|
||||||
|
syncEvt.InvitedRooms = append(syncEvt.InvitedRooms, ir)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (h *HiClient) processSyncJoinedRoom(ctx context.Context, roomID id.RoomID, room *mautrix.SyncJoinedRoom) error {
|
func (h *HiClient) processSyncJoinedRoom(ctx context.Context, roomID id.RoomID, room *mautrix.SyncJoinedRoom) error {
|
||||||
existingRoomData, err := h.DB.Room.Get(ctx, roomID)
|
existingRoomData, err := h.DB.Room.Get(ctx, roomID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -259,6 +293,10 @@ func (h *HiClient) processSyncLeftRoom(ctx context.Context, roomID id.RoomID, ro
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to delete room: %w", err)
|
return fmt.Errorf("failed to delete room: %w", err)
|
||||||
}
|
}
|
||||||
|
err = h.DB.InvitedRoom.Delete(ctx, roomID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete invited room: %w", err)
|
||||||
|
}
|
||||||
payload := ctx.Value(syncContextKey).(*syncContext).evt
|
payload := ctx.Value(syncContextKey).(*syncContext).evt
|
||||||
payload.LeftRooms = append(payload.LeftRooms, roomID)
|
payload.LeftRooms = append(payload.LeftRooms, roomID)
|
||||||
return nil
|
return nil
|
||||||
|
@ -288,20 +326,34 @@ func removeReplyFallback(evt *event.Event) []byte {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HiClient) decryptEvent(ctx context.Context, evt *event.Event) (*event.Event, []byte, string, error) {
|
func (h *HiClient) decryptEvent(ctx context.Context, evt *event.Event) (*event.Event, []byte, bool, string, error) {
|
||||||
err := evt.Content.ParseRaw(evt.Type)
|
err := evt.Content.ParseRaw(evt.Type)
|
||||||
if err != nil && !errors.Is(err, event.ErrContentAlreadyParsed) {
|
if err != nil && !errors.Is(err, event.ErrContentAlreadyParsed) {
|
||||||
return nil, nil, "", err
|
return nil, nil, false, "", err
|
||||||
}
|
}
|
||||||
decrypted, err := h.Crypto.DecryptMegolmEvent(ctx, evt)
|
decrypted, err := h.Crypto.DecryptMegolmEvent(ctx, evt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, "", err
|
return nil, nil, false, "", err
|
||||||
}
|
}
|
||||||
withoutFallback := removeReplyFallback(decrypted)
|
withoutFallback := removeReplyFallback(decrypted)
|
||||||
if withoutFallback != nil {
|
if withoutFallback != nil {
|
||||||
return decrypted, withoutFallback, decrypted.Type.Type, nil
|
return decrypted, withoutFallback, true, decrypted.Type.Type, nil
|
||||||
}
|
}
|
||||||
return decrypted, decrypted.Content.VeryRaw, decrypted.Type.Type, nil
|
return decrypted, decrypted.Content.VeryRaw, false, decrypted.Type.Type, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HiClient) decryptEventInto(ctx context.Context, evt *event.Event, dbEvt *database.Event) (*event.Event, error) {
|
||||||
|
decryptedEvt, rawContent, fallbackRemoved, decryptedType, err := h.decryptEvent(ctx, evt)
|
||||||
|
if err != nil {
|
||||||
|
dbEvt.DecryptionError = err.Error()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dbEvt.Decrypted = rawContent
|
||||||
|
if fallbackRemoved {
|
||||||
|
dbEvt.MarkReplyFallbackRemoved()
|
||||||
|
}
|
||||||
|
dbEvt.DecryptedType = decryptedType
|
||||||
|
return decryptedEvt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HiClient) addMediaCache(
|
func (h *HiClient) addMediaCache(
|
||||||
|
@ -344,12 +396,7 @@ func (h *HiClient) addMediaCache(
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HiClient) cacheMedia(ctx context.Context, evt *event.Event, rowID database.EventRowID) {
|
func (h *HiClient) cacheMedia(ctx context.Context, evt *event.Event, rowID database.EventRowID) {
|
||||||
switch evt.Type {
|
cacheMessageEventContent := func(content *event.MessageEventContent) {
|
||||||
case event.EventMessage, event.EventSticker:
|
|
||||||
content, ok := evt.Content.Parsed.(*event.MessageEventContent)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if content.File != nil {
|
if content.File != nil {
|
||||||
h.addMediaCache(ctx, rowID, content.File.URL, content.File, content.Info, content.GetFileName())
|
h.addMediaCache(ctx, rowID, content.File.URL, content.File, content.Info, content.GetFileName())
|
||||||
} else if content.URL != "" {
|
} else if content.URL != "" {
|
||||||
|
@ -360,6 +407,35 @@ func (h *HiClient) cacheMedia(ctx context.Context, evt *event.Event, rowID datab
|
||||||
} else if content.GetInfo().ThumbnailURL != "" {
|
} else if content.GetInfo().ThumbnailURL != "" {
|
||||||
h.addMediaCache(ctx, rowID, content.Info.ThumbnailURL, nil, content.Info.ThumbnailInfo, "")
|
h.addMediaCache(ctx, rowID, content.Info.ThumbnailURL, nil, content.Info.ThumbnailInfo, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, image := range content.BeeperGalleryImages {
|
||||||
|
h.cacheMedia(ctx, &event.Event{
|
||||||
|
Type: event.EventMessage,
|
||||||
|
Content: event.Content{Parsed: image},
|
||||||
|
}, rowID)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, preview := range content.BeeperLinkPreviews {
|
||||||
|
info := &event.FileInfo{MimeType: preview.ImageType}
|
||||||
|
if preview.ImageEncryption != nil {
|
||||||
|
h.addMediaCache(ctx, rowID, preview.ImageEncryption.URL, preview.ImageEncryption, info, "")
|
||||||
|
} else if preview.ImageURL != "" {
|
||||||
|
h.addMediaCache(ctx, rowID, preview.ImageURL, nil, info, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch evt.Type {
|
||||||
|
case event.EventMessage, event.EventSticker:
|
||||||
|
content, ok := evt.Content.Parsed.(*event.MessageEventContent)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheMessageEventContent(content)
|
||||||
|
if content.NewContent != nil {
|
||||||
|
cacheMessageEventContent(content.NewContent)
|
||||||
|
}
|
||||||
case event.StateRoomAvatar:
|
case event.StateRoomAvatar:
|
||||||
_ = evt.Content.ParseRaw(evt.Type)
|
_ = evt.Content.ParseRaw(evt.Type)
|
||||||
content, ok := evt.Content.Parsed.(*event.RoomAvatarEventContent)
|
content, ok := evt.Content.Parsed.(*event.RoomAvatarEventContent)
|
||||||
|
@ -442,12 +518,13 @@ func (h *HiClient) calculateLocalContent(ctx context.Context, dbEvt *database.Ev
|
||||||
wasPlaintext = true
|
wasPlaintext = true
|
||||||
}
|
}
|
||||||
return &database.LocalContent{
|
return &database.LocalContent{
|
||||||
SanitizedHTML: sanitizedHTML,
|
SanitizedHTML: sanitizedHTML,
|
||||||
HTMLVersion: CurrentHTMLSanitizerVersion,
|
HTMLVersion: CurrentHTMLSanitizerVersion,
|
||||||
WasPlaintext: wasPlaintext,
|
WasPlaintext: wasPlaintext,
|
||||||
BigEmoji: bigEmoji,
|
BigEmoji: bigEmoji,
|
||||||
HasMath: hasMath,
|
HasMath: hasMath,
|
||||||
EditSource: editSource,
|
EditSource: editSource,
|
||||||
|
ReplyFallbackRemoved: dbEvt.LocalContent.GetReplyFallbackRemoved(),
|
||||||
}, inlineImages
|
}, inlineImages
|
||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
@ -499,14 +576,12 @@ func (h *HiClient) processEvent(
|
||||||
contentWithoutFallback := removeReplyFallback(evt)
|
contentWithoutFallback := removeReplyFallback(evt)
|
||||||
if contentWithoutFallback != nil {
|
if contentWithoutFallback != nil {
|
||||||
dbEvt.Content = contentWithoutFallback
|
dbEvt.Content = contentWithoutFallback
|
||||||
|
dbEvt.MarkReplyFallbackRemoved()
|
||||||
}
|
}
|
||||||
var decryptionErr error
|
var decryptionErr error
|
||||||
var decryptedMautrixEvt *event.Event
|
var decryptedMautrixEvt *event.Event
|
||||||
if evt.Type == event.EventEncrypted && dbEvt.RedactedBy == "" {
|
if evt.Type == event.EventEncrypted && dbEvt.RedactedBy == "" {
|
||||||
decryptedMautrixEvt, dbEvt.Decrypted, dbEvt.DecryptedType, decryptionErr = h.decryptEvent(ctx, evt)
|
decryptedMautrixEvt, decryptionErr = h.decryptEventInto(ctx, evt, dbEvt)
|
||||||
if decryptionErr != nil {
|
|
||||||
dbEvt.DecryptionError = decryptionErr.Error()
|
|
||||||
}
|
|
||||||
} else if evt.Type == event.EventRedaction {
|
} else if evt.Type == event.EventRedaction {
|
||||||
if evt.Redacts != "" && gjson.GetBytes(evt.Content.VeryRaw, "redacts").Str != evt.Redacts.String() {
|
if evt.Redacts != "" && gjson.GetBytes(evt.Content.VeryRaw, "redacts").Str != evt.Redacts.String() {
|
||||||
var err error
|
var err error
|
||||||
|
@ -592,8 +667,10 @@ func (h *HiClient) processStateAndTimeline(
|
||||||
updatedRoom.LazyLoadSummary = summary
|
updatedRoom.LazyLoadSummary = summary
|
||||||
heroesChanged = true
|
heroesChanged = true
|
||||||
}
|
}
|
||||||
|
sdc := &spaceDataCollector{}
|
||||||
decryptionQueue := make(map[id.SessionID]*database.SessionRequest)
|
decryptionQueue := make(map[id.SessionID]*database.SessionRequest)
|
||||||
allNewEvents := make([]*database.Event, 0, len(state.Events)+len(timeline.Events))
|
allNewEvents := make([]*database.Event, 0, len(state.Events)+len(timeline.Events))
|
||||||
|
addedEvents := make(map[database.EventRowID]struct{})
|
||||||
newNotifications := make([]SyncNotification, 0)
|
newNotifications := make([]SyncNotification, 0)
|
||||||
var recalculatePreviewEvent, unreadMessagesWereMaybeRedacted bool
|
var recalculatePreviewEvent, unreadMessagesWereMaybeRedacted bool
|
||||||
var newUnreadCounts database.UnreadCounts
|
var newUnreadCounts database.UnreadCounts
|
||||||
|
@ -608,7 +685,11 @@ func (h *HiClient) processStateAndTimeline(
|
||||||
} else if dbEvt == nil {
|
} else if dbEvt == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
allNewEvents = append(allNewEvents, dbEvt)
|
_, alreadyAdded := addedEvents[dbEvt.RowID]
|
||||||
|
if !alreadyAdded {
|
||||||
|
addedEvents[dbEvt.RowID] = struct{}{}
|
||||||
|
allNewEvents = append(allNewEvents, dbEvt)
|
||||||
|
}
|
||||||
return dbEvt, nil
|
return dbEvt, nil
|
||||||
}
|
}
|
||||||
processRedaction := func(evt *event.Event) error {
|
processRedaction := func(evt *event.Event) error {
|
||||||
|
@ -643,8 +724,11 @@ func (h *HiClient) processStateAndTimeline(
|
||||||
if isUnread {
|
if isUnread {
|
||||||
if dbEvt.UnreadType.Is(database.UnreadTypeNotify) && h.firstSyncReceived {
|
if dbEvt.UnreadType.Is(database.UnreadTypeNotify) && h.firstSyncReceived {
|
||||||
newNotifications = append(newNotifications, SyncNotification{
|
newNotifications = append(newNotifications, SyncNotification{
|
||||||
RowID: dbEvt.RowID,
|
RowID: dbEvt.RowID,
|
||||||
Sound: dbEvt.UnreadType.Is(database.UnreadTypeSound),
|
Sound: dbEvt.UnreadType.Is(database.UnreadTypeSound),
|
||||||
|
Highlight: dbEvt.UnreadType.Is(database.UnreadTypeHighlight),
|
||||||
|
Event: dbEvt,
|
||||||
|
Room: room,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
newUnreadCounts.AddOne(dbEvt.UnreadType)
|
newUnreadCounts.AddOne(dbEvt.UnreadType)
|
||||||
|
@ -670,9 +754,10 @@ func (h *HiClient) processStateAndTimeline(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return -1, fmt.Errorf("failed to save current state event ID %s for %s/%s: %w", evt.ID, evt.Type.Type, *evt.StateKey, err)
|
return -1, fmt.Errorf("failed to save current state event ID %s for %s/%s: %w", evt.ID, evt.Type.Type, *evt.StateKey, err)
|
||||||
}
|
}
|
||||||
processImportantEvent(ctx, evt, room, updatedRoom)
|
processImportantEvent(ctx, evt, room, updatedRoom, dbEvt.RowID, sdc)
|
||||||
}
|
}
|
||||||
allNewEvents = append(allNewEvents, dbEvt)
|
allNewEvents = append(allNewEvents, dbEvt)
|
||||||
|
addedEvents[dbEvt.RowID] = struct{}{}
|
||||||
if evt.Type == event.EventRedaction && evt.Redacts != "" {
|
if evt.Type == event.EventRedaction && evt.Redacts != "" {
|
||||||
err = processRedaction(evt)
|
err = processRedaction(evt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -683,6 +768,11 @@ func (h *HiClient) processStateAndTimeline(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return -1, fmt.Errorf("failed to get relation target of event: %w", err)
|
return -1, fmt.Errorf("failed to get relation target of event: %w", err)
|
||||||
}
|
}
|
||||||
|
} else if replyTo := dbEvt.GetReplyTo(); replyTo != "" {
|
||||||
|
_, err = addOldEvent(0, replyTo)
|
||||||
|
if err != nil {
|
||||||
|
return -1, fmt.Errorf("failed to get reply target of event: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return dbEvt.RowID, nil
|
return dbEvt.RowID, nil
|
||||||
}
|
}
|
||||||
|
@ -702,15 +792,38 @@ func (h *HiClient) processStateAndTimeline(
|
||||||
setNewState(evt.Type, *evt.StateKey, rowID)
|
setNewState(evt.Type, *evt.StateKey, rowID)
|
||||||
}
|
}
|
||||||
var timelineRowTuples []database.TimelineRowTuple
|
var timelineRowTuples []database.TimelineRowTuple
|
||||||
|
receiptMap := make(map[id.EventID][]*database.Receipt)
|
||||||
|
for _, receipt := range receipts {
|
||||||
|
if receipt.UserID != h.Account.UserID {
|
||||||
|
receiptMap[receipt.EventID] = append(receiptMap[receipt.EventID], receipt)
|
||||||
|
}
|
||||||
|
}
|
||||||
var err error
|
var err error
|
||||||
if len(timeline.Events) > 0 {
|
if len(timeline.Events) > 0 {
|
||||||
timelineIDs := make([]database.EventRowID, len(timeline.Events))
|
timelineIDs := make([]database.EventRowID, len(timeline.Events))
|
||||||
|
encounteredReceiptUsers := make(map[id.UserID]struct{})
|
||||||
readUpToIndex := -1
|
readUpToIndex := -1
|
||||||
for i := len(timeline.Events) - 1; i >= 0; i-- {
|
for i := len(timeline.Events) - 1; i >= 0; i-- {
|
||||||
evt := timeline.Events[i]
|
evt := timeline.Events[i]
|
||||||
|
for _, receipt := range receiptMap[evt.ID] {
|
||||||
|
encounteredReceiptUsers[receipt.UserID] = struct{}{}
|
||||||
|
}
|
||||||
isRead := slices.Contains(newOwnReceipts, evt.ID)
|
isRead := slices.Contains(newOwnReceipts, evt.ID)
|
||||||
isOwnEvent := evt.Sender == h.Account.UserID
|
isOwnEvent := evt.Sender == h.Account.UserID
|
||||||
if isRead || isOwnEvent {
|
_, alreadyEncountered := encounteredReceiptUsers[evt.Sender]
|
||||||
|
if !isOwnEvent && !alreadyEncountered {
|
||||||
|
encounteredReceiptUsers[evt.Sender] = struct{}{}
|
||||||
|
injectedReceipt := &database.Receipt{
|
||||||
|
RoomID: room.ID,
|
||||||
|
UserID: evt.Sender,
|
||||||
|
ReceiptType: event.ReceiptTypeRead,
|
||||||
|
EventID: evt.ID,
|
||||||
|
Timestamp: jsontime.UM(time.UnixMilli(evt.Timestamp)),
|
||||||
|
}
|
||||||
|
receipts = append(receipts, injectedReceipt)
|
||||||
|
receiptMap[evt.ID] = append(receiptMap[evt.ID], injectedReceipt)
|
||||||
|
}
|
||||||
|
if readUpToIndex == -1 && (isRead || isOwnEvent) {
|
||||||
readUpToIndex = i
|
readUpToIndex = i
|
||||||
// Reset unread counts if we see our own read receipt in the timeline.
|
// Reset unread counts if we see our own read receipt in the timeline.
|
||||||
// It'll be updated with new unreads (if any) at the end.
|
// It'll be updated with new unreads (if any) at the end.
|
||||||
|
@ -725,7 +838,6 @@ func (h *HiClient) processStateAndTimeline(
|
||||||
})
|
})
|
||||||
newOwnReceipts = append(newOwnReceipts, evt.ID)
|
newOwnReceipts = append(newOwnReceipts, evt.ID)
|
||||||
}
|
}
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for i, evt := range timeline.Events {
|
for i, evt := range timeline.Events {
|
||||||
|
@ -785,10 +897,11 @@ func (h *HiClient) processStateAndTimeline(
|
||||||
}
|
}
|
||||||
// Calculate name from participants if participants changed and current name was generated from participants, or if the room name was unset
|
// Calculate name from participants if participants changed and current name was generated from participants, or if the room name was unset
|
||||||
if (heroesChanged && updatedRoom.NameQuality <= database.NameQualityParticipants) || updatedRoom.NameQuality == database.NameQualityNil {
|
if (heroesChanged && updatedRoom.NameQuality <= database.NameQualityParticipants) || updatedRoom.NameQuality == database.NameQualityNil {
|
||||||
name, dmAvatarURL, err := h.calculateRoomParticipantName(ctx, room.ID, summary)
|
name, dmAvatarURL, dmUserID, err := h.calculateRoomParticipantName(ctx, room.ID, summary)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to calculate room name: %w", err)
|
return fmt.Errorf("failed to calculate room name: %w", err)
|
||||||
}
|
}
|
||||||
|
updatedRoom.DMUserID = &dmUserID
|
||||||
updatedRoom.Name = &name
|
updatedRoom.Name = &name
|
||||||
updatedRoom.NameQuality = database.NameQualityParticipants
|
updatedRoom.NameQuality = database.NameQualityParticipants
|
||||||
if !dmAvatarURL.IsEmpty() && !room.ExplicitAvatar {
|
if !dmAvatarURL.IsEmpty() && !room.ExplicitAvatar {
|
||||||
|
@ -814,6 +927,7 @@ func (h *HiClient) processStateAndTimeline(
|
||||||
} else {
|
} else {
|
||||||
updatedRoom.UnreadCounts.Add(newUnreadCounts)
|
updatedRoom.UnreadCounts.Add(newUnreadCounts)
|
||||||
}
|
}
|
||||||
|
dismissNotifications := room.UnreadNotifications > 0 && updatedRoom.UnreadNotifications == 0 && len(newNotifications) == 0
|
||||||
if timeline.PrevBatch != "" && (room.PrevBatch == "" || timeline.Limited) {
|
if timeline.PrevBatch != "" && (room.PrevBatch == "" || timeline.Limited) {
|
||||||
updatedRoom.PrevBatch = timeline.PrevBatch
|
updatedRoom.PrevBatch = timeline.PrevBatch
|
||||||
}
|
}
|
||||||
|
@ -824,16 +938,26 @@ func (h *HiClient) processStateAndTimeline(
|
||||||
return fmt.Errorf("failed to save room data: %w", err)
|
return fmt.Errorf("failed to save room data: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
err = sdc.Apply(ctx, room, h.DB.SpaceEdge)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
// TODO why is *old* unread count sometimes zero when processing the read receipt that is making it zero?
|
// TODO why is *old* unread count sometimes zero when processing the read receipt that is making it zero?
|
||||||
if roomChanged || len(accountData) > 0 || len(newOwnReceipts) > 0 || len(timelineRowTuples) > 0 || len(allNewEvents) > 0 {
|
if roomChanged || len(accountData) > 0 || len(newOwnReceipts) > 0 || len(receipts) > 0 || len(timelineRowTuples) > 0 || len(allNewEvents) > 0 {
|
||||||
|
for _, receipt := range receipts {
|
||||||
|
receipt.RoomID = ""
|
||||||
|
}
|
||||||
ctx.Value(syncContextKey).(*syncContext).evt.Rooms[room.ID] = &SyncRoom{
|
ctx.Value(syncContextKey).(*syncContext).evt.Rooms[room.ID] = &SyncRoom{
|
||||||
Meta: room,
|
Meta: room,
|
||||||
Timeline: timelineRowTuples,
|
Timeline: timelineRowTuples,
|
||||||
AccountData: accountData,
|
AccountData: accountData,
|
||||||
State: changedState,
|
State: changedState,
|
||||||
Reset: timeline.Limited,
|
Reset: timeline.Limited,
|
||||||
Events: allNewEvents,
|
Events: allNewEvents,
|
||||||
Notifications: newNotifications,
|
Receipts: receiptMap,
|
||||||
|
|
||||||
|
Notifications: newNotifications,
|
||||||
|
DismissNotifications: dismissNotifications,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -849,15 +973,15 @@ func joinMemberNames(names []string, totalCount int) string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HiClient) calculateRoomParticipantName(ctx context.Context, roomID id.RoomID, summary *mautrix.LazyLoadSummary) (string, id.ContentURI, error) {
|
func (h *HiClient) calculateRoomParticipantName(ctx context.Context, roomID id.RoomID, summary *mautrix.LazyLoadSummary) (string, id.ContentURI, id.UserID, error) {
|
||||||
var primaryAvatarURL id.ContentURI
|
var primaryAvatarURL id.ContentURI
|
||||||
if summary == nil || len(summary.Heroes) == 0 {
|
if summary == nil || len(summary.Heroes) == 0 {
|
||||||
return "Empty room", primaryAvatarURL, nil
|
return "Empty room", primaryAvatarURL, "", nil
|
||||||
}
|
}
|
||||||
var functionalMembers []id.UserID
|
var functionalMembers []id.UserID
|
||||||
functionalMembersEvt, err := h.DB.CurrentState.Get(ctx, roomID, event.StateElementFunctionalMembers, "")
|
functionalMembersEvt, err := h.DB.CurrentState.Get(ctx, roomID, event.StateElementFunctionalMembers, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", primaryAvatarURL, fmt.Errorf("failed to get %s event: %w", event.StateElementFunctionalMembers.Type, err)
|
return "", primaryAvatarURL, "", fmt.Errorf("failed to get %s event: %w", event.StateElementFunctionalMembers.Type, err)
|
||||||
} else if functionalMembersEvt != nil {
|
} else if functionalMembersEvt != nil {
|
||||||
mautrixEvt := functionalMembersEvt.AsRawMautrix()
|
mautrixEvt := functionalMembersEvt.AsRawMautrix()
|
||||||
_ = mautrixEvt.Content.ParseRaw(mautrixEvt.Type)
|
_ = mautrixEvt.Content.ParseRaw(mautrixEvt.Type)
|
||||||
|
@ -873,16 +997,21 @@ func (h *HiClient) calculateRoomParticipantName(ctx context.Context, roomID id.R
|
||||||
} else if summary.InvitedMemberCount != nil {
|
} else if summary.InvitedMemberCount != nil {
|
||||||
memberCount = *summary.InvitedMemberCount
|
memberCount = *summary.InvitedMemberCount
|
||||||
}
|
}
|
||||||
|
var dmUserID id.UserID
|
||||||
for _, hero := range summary.Heroes {
|
for _, hero := range summary.Heroes {
|
||||||
if slices.Contains(functionalMembers, hero) {
|
if slices.Contains(functionalMembers, hero) {
|
||||||
|
// TODO save member count so push rule evaluation would use the subtracted one?
|
||||||
memberCount--
|
memberCount--
|
||||||
continue
|
continue
|
||||||
} else if len(members) >= 5 {
|
} else if len(members) >= 5 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
if dmUserID == "" {
|
||||||
|
dmUserID = hero
|
||||||
|
}
|
||||||
heroEvt, err := h.DB.CurrentState.Get(ctx, roomID, event.StateMember, hero.String())
|
heroEvt, err := h.DB.CurrentState.Get(ctx, roomID, event.StateMember, hero.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", primaryAvatarURL, fmt.Errorf("failed to get %s's member event: %w", hero, err)
|
return "", primaryAvatarURL, "", fmt.Errorf("failed to get %s's member event: %w", hero, err)
|
||||||
} else if heroEvt == nil {
|
} else if heroEvt == nil {
|
||||||
leftMembers = append(leftMembers, hero.String())
|
leftMembers = append(leftMembers, hero.String())
|
||||||
continue
|
continue
|
||||||
|
@ -898,19 +1027,28 @@ func (h *HiClient) calculateRoomParticipantName(ctx context.Context, roomID id.R
|
||||||
}
|
}
|
||||||
if membership == "join" || membership == "invite" {
|
if membership == "join" || membership == "invite" {
|
||||||
members = append(members, name)
|
members = append(members, name)
|
||||||
|
dmUserID = hero
|
||||||
} else {
|
} else {
|
||||||
leftMembers = append(leftMembers, name)
|
leftMembers = append(leftMembers, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(members)+len(leftMembers) > 1 || !primaryAvatarURL.IsValid() {
|
if !primaryAvatarURL.IsValid() {
|
||||||
primaryAvatarURL = id.ContentURI{}
|
primaryAvatarURL = id.ContentURI{}
|
||||||
}
|
}
|
||||||
if len(members) > 0 {
|
if len(members) > 0 {
|
||||||
return joinMemberNames(members, memberCount), primaryAvatarURL, nil
|
if len(members) > 1 {
|
||||||
|
primaryAvatarURL = id.ContentURI{}
|
||||||
|
dmUserID = ""
|
||||||
|
}
|
||||||
|
return joinMemberNames(members, memberCount), primaryAvatarURL, dmUserID, nil
|
||||||
} else if len(leftMembers) > 0 {
|
} else if len(leftMembers) > 0 {
|
||||||
return fmt.Sprintf("Empty room (was %s)", joinMemberNames(leftMembers, memberCount)), primaryAvatarURL, nil
|
if len(leftMembers) > 1 {
|
||||||
|
primaryAvatarURL = id.ContentURI{}
|
||||||
|
dmUserID = ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("Empty room (was %s)", joinMemberNames(leftMembers, memberCount)), primaryAvatarURL, "", nil
|
||||||
} else {
|
} else {
|
||||||
return "Empty room", primaryAvatarURL, nil
|
return "Empty room", primaryAvatarURL, "", nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -921,20 +1059,112 @@ func intPtrEqual(a, b *int) bool {
|
||||||
return *a == *b
|
return *a == *b
|
||||||
}
|
}
|
||||||
|
|
||||||
func processImportantEvent(ctx context.Context, evt *event.Event, existingRoomData, updatedRoom *database.Room) (roomDataChanged bool) {
|
type spaceDataCollector struct {
|
||||||
|
Children []database.SpaceChildEntry
|
||||||
|
Parents []database.SpaceParentEntry
|
||||||
|
RemovedChildren []id.RoomID
|
||||||
|
RemovedParents []id.RoomID
|
||||||
|
PowerLevelChanged bool
|
||||||
|
IsFullState bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sdc *spaceDataCollector) Collect(evt *event.Event, rowID database.EventRowID) {
|
||||||
|
switch evt.Type {
|
||||||
|
case event.StatePowerLevels:
|
||||||
|
sdc.PowerLevelChanged = true
|
||||||
|
case event.StateCreate:
|
||||||
|
sdc.IsFullState = true
|
||||||
|
case event.StateSpaceChild:
|
||||||
|
content := evt.Content.AsSpaceChild()
|
||||||
|
if len(content.Via) == 0 {
|
||||||
|
sdc.RemovedChildren = append(sdc.RemovedChildren, id.RoomID(*evt.StateKey))
|
||||||
|
} else {
|
||||||
|
sdc.Children = append(sdc.Children, database.SpaceChildEntry{
|
||||||
|
ChildID: id.RoomID(*evt.StateKey),
|
||||||
|
EventRowID: rowID,
|
||||||
|
Order: content.Order,
|
||||||
|
Suggested: content.Suggested,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
case event.StateSpaceParent:
|
||||||
|
content := evt.Content.AsSpaceParent()
|
||||||
|
if len(content.Via) == 0 {
|
||||||
|
sdc.RemovedParents = append(sdc.RemovedParents, id.RoomID(*evt.StateKey))
|
||||||
|
} else {
|
||||||
|
sdc.Parents = append(sdc.Parents, database.SpaceParentEntry{
|
||||||
|
ParentID: id.RoomID(*evt.StateKey),
|
||||||
|
EventRowID: rowID,
|
||||||
|
Canonical: content.Canonical,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sdc *spaceDataCollector) Apply(ctx context.Context, room *database.Room, seq *database.SpaceEdgeQuery) error {
|
||||||
|
if room.CreationContent == nil || room.CreationContent.Type != event.RoomTypeSpace {
|
||||||
|
sdc.Children = nil
|
||||||
|
sdc.RemovedChildren = nil
|
||||||
|
sdc.PowerLevelChanged = false
|
||||||
|
}
|
||||||
|
if len(sdc.Children) == 0 && len(sdc.RemovedChildren) == 0 &&
|
||||||
|
len(sdc.Parents) == 0 && len(sdc.RemovedParents) == 0 &&
|
||||||
|
!sdc.PowerLevelChanged {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return seq.GetDB().DoTxn(ctx, nil, func(ctx context.Context) error {
|
||||||
|
if len(sdc.Children) > 0 || len(sdc.RemovedChildren) > 0 {
|
||||||
|
err := seq.SetChildren(ctx, room.ID, sdc.Children, sdc.RemovedChildren, sdc.IsFullState)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to set space children: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(sdc.Parents) > 0 || len(sdc.RemovedParents) > 0 {
|
||||||
|
err := seq.SetParents(ctx, room.ID, sdc.Parents, sdc.RemovedParents, sdc.IsFullState)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to set space parents: %w", err)
|
||||||
|
}
|
||||||
|
if len(sdc.Parents) > 0 {
|
||||||
|
err = seq.RevalidateAllParentsOfRoomValidity(ctx, room.ID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to revalidate own parent references: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if sdc.PowerLevelChanged {
|
||||||
|
err := seq.RevalidateAllChildrenOfParentValidity(ctx, room.ID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to revalidate child parent references to self: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func processImportantEvent(
|
||||||
|
ctx context.Context,
|
||||||
|
evt *event.Event,
|
||||||
|
existingRoomData, updatedRoom *database.Room,
|
||||||
|
rowID database.EventRowID,
|
||||||
|
sdc *spaceDataCollector,
|
||||||
|
) (roomDataChanged bool) {
|
||||||
if evt.StateKey == nil {
|
if evt.StateKey == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
switch evt.Type {
|
switch evt.Type {
|
||||||
case event.StateCreate, event.StateTombstone, event.StateRoomName, event.StateCanonicalAlias,
|
case event.StateCreate, event.StateTombstone, event.StateRoomName, event.StateCanonicalAlias,
|
||||||
event.StateRoomAvatar, event.StateTopic, event.StateEncryption:
|
event.StateRoomAvatar, event.StateTopic, event.StateEncryption, event.StatePowerLevels:
|
||||||
if *evt.StateKey != "" {
|
if *evt.StateKey != "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
case event.StateSpaceChild, event.StateSpaceParent:
|
||||||
|
if !strings.HasPrefix(*evt.StateKey, "!") {
|
||||||
|
return
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := evt.Content.ParseRaw(evt.Type)
|
err := evt.Content.ParseRaw(evt.Type)
|
||||||
|
sdc.Collect(evt, rowID)
|
||||||
if err != nil && !errors.Is(err, event.ErrContentAlreadyParsed) {
|
if err != nil && !errors.Is(err, event.ErrContentAlreadyParsed) {
|
||||||
zerolog.Ctx(ctx).Warn().Err(err).
|
zerolog.Ctx(ctx).Warn().Err(err).
|
||||||
Stringer("event_type", &evt.Type).
|
Stringer("event_type", &evt.Type).
|
||||||
|
|
|
@ -8,11 +8,16 @@ package hicli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/mattn/go-sqlite3"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
"maunium.net/go/mautrix"
|
"maunium.net/go/mautrix"
|
||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
|
|
||||||
|
"go.mau.fi/gomuks/pkg/hicli/database"
|
||||||
)
|
)
|
||||||
|
|
||||||
type hiSyncer HiClient
|
type hiSyncer HiClient
|
||||||
|
@ -29,19 +34,29 @@ func (h *hiSyncer) ProcessResponse(ctx context.Context, resp *mautrix.RespSync,
|
||||||
c := (*HiClient)(h)
|
c := (*HiClient)(h)
|
||||||
c.lastSync = time.Now()
|
c.lastSync = time.Now()
|
||||||
ctx = context.WithValue(ctx, syncContextKey, &syncContext{evt: &SyncComplete{
|
ctx = context.WithValue(ctx, syncContextKey, &syncContext{evt: &SyncComplete{
|
||||||
Since: &since,
|
Since: &since,
|
||||||
Rooms: make(map[id.RoomID]*SyncRoom, len(resp.Rooms.Join)),
|
Rooms: make(map[id.RoomID]*SyncRoom, len(resp.Rooms.Join)),
|
||||||
LeftRooms: make([]id.RoomID, 0, len(resp.Rooms.Leave)),
|
InvitedRooms: make([]*database.InvitedRoom, 0, len(resp.Rooms.Invite)),
|
||||||
|
LeftRooms: make([]id.RoomID, 0, len(resp.Rooms.Leave)),
|
||||||
}})
|
}})
|
||||||
err := c.preProcessSyncResponse(ctx, resp, since)
|
err := c.preProcessSyncResponse(ctx, resp, since)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = c.DB.DoTxn(ctx, nil, func(ctx context.Context) error {
|
for i := 0; ; i++ {
|
||||||
return c.processSyncResponse(ctx, resp, since)
|
err = c.DB.DoTxn(ctx, nil, func(ctx context.Context) error {
|
||||||
})
|
return c.processSyncResponse(ctx, resp, since)
|
||||||
if err != nil {
|
})
|
||||||
return err
|
var sqliteErr sqlite3.Error
|
||||||
|
if errors.As(err, &sqliteErr) && sqliteErr.Code == sqlite3.ErrBusy && i < 24 {
|
||||||
|
zerolog.Ctx(ctx).Warn().Err(err).Msg("Database is busy, retrying")
|
||||||
|
c.markSyncErrored(err, false)
|
||||||
|
continue
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
c.postProcessSyncResponse(ctx, resp, since)
|
c.postProcessSyncResponse(ctx, resp, since)
|
||||||
c.syncErrors = 0
|
c.syncErrors = 0
|
||||||
|
@ -56,7 +71,7 @@ func (h *hiSyncer) OnFailedSync(_ *mautrix.RespSync, err error) (time.Duration,
|
||||||
if c.syncErrors > 5 {
|
if c.syncErrors > 5 {
|
||||||
delay = max(time.Duration(c.syncErrors)*time.Second, 30*time.Second)
|
delay = max(time.Duration(c.syncErrors)*time.Second, 30*time.Second)
|
||||||
}
|
}
|
||||||
c.markSyncErrored(err)
|
c.markSyncErrored(err, false)
|
||||||
c.Log.Err(err).Dur("retry_in", delay).Msg("Sync failed")
|
c.Log.Err(err).Dur("retry_in", delay).Msg("Sync failed")
|
||||||
return delay, nil
|
return delay, nil
|
||||||
}
|
}
|
||||||
|
@ -64,23 +79,23 @@ func (h *hiSyncer) OnFailedSync(_ *mautrix.RespSync, err error) (time.Duration,
|
||||||
func (h *hiSyncer) GetFilterJSON(_ id.UserID) *mautrix.Filter {
|
func (h *hiSyncer) GetFilterJSON(_ id.UserID) *mautrix.Filter {
|
||||||
if !h.Verified {
|
if !h.Verified {
|
||||||
return &mautrix.Filter{
|
return &mautrix.Filter{
|
||||||
Presence: mautrix.FilterPart{
|
Presence: &mautrix.FilterPart{
|
||||||
NotRooms: []id.RoomID{"*"},
|
NotRooms: []id.RoomID{"*"},
|
||||||
},
|
},
|
||||||
Room: mautrix.RoomFilter{
|
Room: &mautrix.RoomFilter{
|
||||||
NotRooms: []id.RoomID{"*"},
|
NotRooms: []id.RoomID{"*"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &mautrix.Filter{
|
return &mautrix.Filter{
|
||||||
Presence: mautrix.FilterPart{
|
Presence: &mautrix.FilterPart{
|
||||||
NotRooms: []id.RoomID{"*"},
|
NotRooms: []id.RoomID{"*"},
|
||||||
},
|
},
|
||||||
Room: mautrix.RoomFilter{
|
Room: &mautrix.RoomFilter{
|
||||||
State: mautrix.FilterPart{
|
State: &mautrix.FilterPart{
|
||||||
LazyLoadMembers: true,
|
LazyLoadMembers: true,
|
||||||
},
|
},
|
||||||
Timeline: mautrix.FilterPart{
|
Timeline: &mautrix.FilterPart{
|
||||||
Limit: 100,
|
Limit: 100,
|
||||||
LazyLoadMembers: true,
|
LazyLoadMembers: true,
|
||||||
},
|
},
|
||||||
|
|
|
@ -72,5 +72,5 @@ func init() {
|
||||||
builtWith = fmt.Sprintf("built at %s with %s", BuildTime, runtime.Version())
|
builtWith = fmt.Sprintf("built at %s with %s", BuildTime, runtime.Version())
|
||||||
}
|
}
|
||||||
mautrix.DefaultUserAgent = fmt.Sprintf("gomuks/%s %s", Version, mautrix.DefaultUserAgent)
|
mautrix.DefaultUserAgent = fmt.Sprintf("gomuks/%s %s", Version, mautrix.DefaultUserAgent)
|
||||||
Description = fmt.Sprintf("gomuks %s (%s)", Version, builtWith)
|
Description = fmt.Sprintf("gomuks %s on %s/%s (%s)", Version, runtime.GOOS, runtime.GOARCH, builtWith)
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,8 +73,9 @@ export default tseslint.config(
|
||||||
"one-var-declaration-per-line": ["error", "initializations"],
|
"one-var-declaration-per-line": ["error", "initializations"],
|
||||||
"quotes": ["error", "double", {allowTemplateLiterals: true}],
|
"quotes": ["error", "double", {allowTemplateLiterals: true}],
|
||||||
"semi": ["error", "never"],
|
"semi": ["error", "never"],
|
||||||
|
"curly": ["error", "all"],
|
||||||
"comma-dangle": ["error", "always-multiline"],
|
"comma-dangle": ["error", "always-multiline"],
|
||||||
"max-len": ["warn", 120],
|
"max-len": ["error", 120],
|
||||||
"space-before-function-paren": ["error", {
|
"space-before-function-paren": ["error", {
|
||||||
"anonymous": "never",
|
"anonymous": "never",
|
||||||
"named": "never",
|
"named": "never",
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en" data-gomuks="true">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8"/>
|
<meta charset="UTF-8"/>
|
||||||
<link rel="icon" type="image/png" href="gomuks.png"/>
|
<link id="favicon" rel="icon" type="image/png" href="gomuks.png"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
<link rel="manifest" href="manifest.json"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, interactive-widget=resizes-content"/>
|
||||||
<title>gomuks web</title>
|
<title>gomuks web</title>
|
||||||
<!-- etag placeholder -->
|
<!-- etag placeholder -->
|
||||||
</head>
|
</head>
|
||||||
|
@ -11,5 +12,16 @@
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="src/main.tsx"></script>
|
<script type="module" src="src/main.tsx"></script>
|
||||||
<audio id="default-notification-sound" preload="auto" src="sounds/bright.flac"></audio>
|
<audio id="default-notification-sound" preload="auto" src="sounds/bright.flac"></audio>
|
||||||
|
<svg style="position: absolute; width: 0; height: 0;" viewBox="0 0 1 1" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<clipPath id="squircle" clipPathUnits="objectBoundingBox">
|
||||||
|
<path d="M 0,0.5
|
||||||
|
C 0,0 0,0 0.5,0
|
||||||
|
1,0 1,0 1,0.5
|
||||||
|
1,1 1,1 0.5,1
|
||||||
|
0,1 0,1 0,0.5"></path>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
1358
web/package-lock.json
generated
BIN
web/public/gomuks-maskable-transparent.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
web/public/gomuks-maskable.png
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
web/public/gomuks-transparent.png
Normal file
After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 34 KiB |
32
web/public/manifest.json
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"name": "gomuks web",
|
||||||
|
"description": "A Matrix client written in Go",
|
||||||
|
"categories": ["social", "productivity"],
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "gomuks-transparent.png",
|
||||||
|
"sizes": "880x880",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "monochrome"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "gomuks-maskable.png",
|
||||||
|
"sizes": "1200x1200",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "gomuks.png",
|
||||||
|
"sizes": "880x880",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"protocol_handlers": [{
|
||||||
|
"protocol": "matrix",
|
||||||
|
"url": "#/uri/%s"
|
||||||
|
}],
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#00c853"
|
||||||
|
}
|
|
@ -13,7 +13,7 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import { useEffect, useLayoutEffect, useMemo } from "react"
|
import { useEffect, useMemo } from "react"
|
||||||
import { ScaleLoader } from "react-spinners"
|
import { ScaleLoader } from "react-spinners"
|
||||||
import Client from "./api/client.ts"
|
import Client from "./api/client.ts"
|
||||||
import RPCClient from "./api/rpc.ts"
|
import RPCClient from "./api/rpc.ts"
|
||||||
|
@ -22,7 +22,7 @@ import WSClient from "./api/wsclient.ts"
|
||||||
import ClientContext from "./ui/ClientContext.ts"
|
import ClientContext from "./ui/ClientContext.ts"
|
||||||
import MainScreen from "./ui/MainScreen.tsx"
|
import MainScreen from "./ui/MainScreen.tsx"
|
||||||
import { LoginScreen, VerificationScreen } from "./ui/login"
|
import { LoginScreen, VerificationScreen } from "./ui/login"
|
||||||
import { LightboxWrapper } from "./ui/modal/Lightbox.tsx"
|
import { LightboxWrapper } from "./ui/modal"
|
||||||
import { useEventAsState } from "./util/eventdispatcher.ts"
|
import { useEventAsState } from "./util/eventdispatcher.ts"
|
||||||
|
|
||||||
function makeRPCClient(): RPCClient {
|
function makeRPCClient(): RPCClient {
|
||||||
|
@ -36,10 +36,10 @@ function App() {
|
||||||
const client = useMemo(() => new Client(makeRPCClient()), [])
|
const client = useMemo(() => new Client(makeRPCClient()), [])
|
||||||
const connState = useEventAsState(client.rpc.connect)
|
const connState = useEventAsState(client.rpc.connect)
|
||||||
const clientState = useEventAsState(client.state)
|
const clientState = useEventAsState(client.state)
|
||||||
useLayoutEffect(() => {
|
useEffect(() => {
|
||||||
window.client = client
|
window.client = client
|
||||||
|
return client.start()
|
||||||
}, [client])
|
}, [client])
|
||||||
useEffect(() => client.start(), [client])
|
|
||||||
|
|
||||||
const afterConnectError = Boolean(connState?.error && connState.reconnecting && clientState?.is_verified)
|
const afterConnectError = Boolean(connState?.error && connState.reconnecting && clientState?.is_verified)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -70,18 +70,18 @@ function App() {
|
||||||
</div> : null
|
</div> : null
|
||||||
|
|
||||||
if (connState?.error && !afterConnectError) {
|
if (connState?.error && !afterConnectError) {
|
||||||
return errorOverlay
|
return <div className="pre-main">{errorOverlay}</div>
|
||||||
} else if ((!connState?.connected && !afterConnectError) || !clientState) {
|
} else if ((!connState?.connected && !afterConnectError) || !clientState) {
|
||||||
const msg = connState?.connected ?
|
const msg = connState?.connected ?
|
||||||
"Waiting for client state..." : "Connecting to backend..."
|
"Waiting for client state..." : "Connecting to backend..."
|
||||||
return <div className="pre-connect">
|
return <div className="pre-main waiting-to-connect">
|
||||||
<ScaleLoader width="2rem" height="2rem" color="var(--primary-color)"/>
|
<ScaleLoader width="2rem" height="2rem" color="var(--primary-color)"/>
|
||||||
{msg}
|
{msg}
|
||||||
</div>
|
</div>
|
||||||
} else if (!clientState.is_logged_in) {
|
} else if (!clientState.is_logged_in) {
|
||||||
return <LoginScreen client={client} clientState={clientState}/>
|
return <div className="pre-main"><LoginScreen client={client} clientState={clientState}/></div>
|
||||||
} else if (!clientState.is_verified) {
|
} else if (!clientState.is_verified) {
|
||||||
return <VerificationScreen client={client} clientState={clientState}/>
|
return <div className="pre-main"><VerificationScreen client={client} clientState={clientState}/></div>
|
||||||
} else {
|
} else {
|
||||||
return <ClientContext value={client}>
|
return <ClientContext value={client}>
|
||||||
<LightboxWrapper>
|
<LightboxWrapper>
|
||||||
|
|
|
@ -22,6 +22,7 @@ import type {
|
||||||
ElementRecentEmoji,
|
ElementRecentEmoji,
|
||||||
EventID,
|
EventID,
|
||||||
EventType,
|
EventType,
|
||||||
|
GomuksAndroidMessageToWeb,
|
||||||
ImagePackRooms,
|
ImagePackRooms,
|
||||||
RPCEvent,
|
RPCEvent,
|
||||||
RawDBEvent,
|
RawDBEvent,
|
||||||
|
@ -37,7 +38,7 @@ export default class Client {
|
||||||
readonly initComplete = new NonNullCachedEventDispatcher<boolean>(false)
|
readonly initComplete = new NonNullCachedEventDispatcher<boolean>(false)
|
||||||
readonly store = new StateStore()
|
readonly store = new StateStore()
|
||||||
#stateRequests: RoomStateGUID[] = []
|
#stateRequests: RoomStateGUID[] = []
|
||||||
#stateRequestQueued = false
|
#stateRequestPromise: Promise<void> | null = null
|
||||||
#gcInterval: number | undefined
|
#gcInterval: number | undefined
|
||||||
|
|
||||||
constructor(readonly rpc: RPCClient) {
|
constructor(readonly rpc: RPCClient) {
|
||||||
|
@ -71,6 +72,74 @@ export default class Client {
|
||||||
this.requestNotificationPermission()
|
this.requestNotificationPermission()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async #reallyStartAndroid(signal: AbortSignal) {
|
||||||
|
const androidListener = async (evt: CustomEventInit<string>) => {
|
||||||
|
const evtData = JSON.parse(evt.detail ?? "{}") as GomuksAndroidMessageToWeb
|
||||||
|
switch (evtData.type) {
|
||||||
|
case "register_push":
|
||||||
|
await this.rpc.registerPush({
|
||||||
|
type: "fcm",
|
||||||
|
device_id: evtData.device_id,
|
||||||
|
data: evtData.token,
|
||||||
|
encryption: evtData.encryption,
|
||||||
|
expiration: evtData.expiration,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
case "auth":
|
||||||
|
try {
|
||||||
|
const resp = await fetch("_gomuks/auth?no_prompt=true", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: evtData.authorization,
|
||||||
|
},
|
||||||
|
signal,
|
||||||
|
})
|
||||||
|
if (!resp.ok && !signal.aborted) {
|
||||||
|
console.error("Failed to authenticate:", resp.status, resp.statusText)
|
||||||
|
window.dispatchEvent(new CustomEvent("GomuksWebMessageToAndroid", {
|
||||||
|
detail: {
|
||||||
|
event: "auth_fail",
|
||||||
|
error: `${resp.statusText || resp.status}`,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to authenticate:", err)
|
||||||
|
window.dispatchEvent(new CustomEvent("GomuksWebMessageToAndroid", {
|
||||||
|
detail: {
|
||||||
|
event: "auth_fail",
|
||||||
|
error: `${err}`.replace(/^Error: /, ""),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (signal.aborted) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log("Successfully authenticated, connecting to websocket")
|
||||||
|
this.rpc.start()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const unsubscribeConnect = this.rpc.connect.listen(evt => {
|
||||||
|
if (!evt.connected) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
window.dispatchEvent(new CustomEvent("GomuksWebMessageToAndroid", {
|
||||||
|
detail: { event: "connected" },
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
window.addEventListener("GomuksAndroidMessageToWeb", androidListener)
|
||||||
|
signal.addEventListener("abort", () => {
|
||||||
|
unsubscribeConnect()
|
||||||
|
window.removeEventListener("GomuksAndroidMessageToWeb", androidListener)
|
||||||
|
})
|
||||||
|
window.dispatchEvent(new CustomEvent("GomuksWebMessageToAndroid", {
|
||||||
|
detail: { event: "ready" },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
requestNotificationPermission = (evt?: MouseEvent) => {
|
requestNotificationPermission = (evt?: MouseEvent) => {
|
||||||
window.Notification?.requestPermission().then(permission => {
|
window.Notification?.requestPermission().then(permission => {
|
||||||
console.log("Notification permission:", permission)
|
console.log("Notification permission:", permission)
|
||||||
|
@ -86,7 +155,11 @@ export default class Client {
|
||||||
|
|
||||||
start(): () => void {
|
start(): () => void {
|
||||||
const abort = new AbortController()
|
const abort = new AbortController()
|
||||||
this.#reallyStart(abort.signal)
|
if (window.gomuksAndroid) {
|
||||||
|
this.#reallyStartAndroid(abort.signal)
|
||||||
|
} else {
|
||||||
|
this.#reallyStart(abort.signal)
|
||||||
|
}
|
||||||
this.#gcInterval = setInterval(() => {
|
this.#gcInterval = setInterval(() => {
|
||||||
console.log("Garbage collection completed:", this.store.doGarbageCollection())
|
console.log("Garbage collection completed:", this.store.doGarbageCollection())
|
||||||
}, window.gcSettings.interval)
|
}, window.gcSettings.interval)
|
||||||
|
@ -104,6 +177,7 @@ export default class Client {
|
||||||
#handleEvent = (ev: RPCEvent) => {
|
#handleEvent = (ev: RPCEvent) => {
|
||||||
if (ev.command === "client_state") {
|
if (ev.command === "client_state") {
|
||||||
this.state.emit(ev.data)
|
this.state.emit(ev.data)
|
||||||
|
this.store.userID = ev.data.is_logged_in ? ev.data.user_id : ""
|
||||||
} else if (ev.command === "sync_status") {
|
} else if (ev.command === "sync_status") {
|
||||||
this.syncStatus.emit(ev.data)
|
this.syncStatus.emit(ev.data)
|
||||||
} else if (ev.command === "init_complete") {
|
} else if (ev.command === "init_complete") {
|
||||||
|
@ -116,6 +190,8 @@ export default class Client {
|
||||||
this.store.applySendComplete(ev.data)
|
this.store.applySendComplete(ev.data)
|
||||||
} else if (ev.command === "image_auth_token") {
|
} else if (ev.command === "image_auth_token") {
|
||||||
this.store.imageAuthToken = ev.data
|
this.store.imageAuthToken = ev.data
|
||||||
|
} else if (ev.command === "typing") {
|
||||||
|
this.store.applyTyping(ev.data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,21 +200,25 @@ export default class Client {
|
||||||
room = this.store.rooms.get(room)
|
room = this.store.rooms.get(room)
|
||||||
}
|
}
|
||||||
if (!room || room.state.get("m.room.member")?.has(userID) || room.requestedMembers.has(userID)) {
|
if (!room || room.state.get("m.room.member")?.has(userID) || room.requestedMembers.has(userID)) {
|
||||||
return
|
return null
|
||||||
}
|
}
|
||||||
room.requestedMembers.add(userID)
|
room.requestedMembers.add(userID)
|
||||||
this.#stateRequests.push({ room_id: room.roomID, type: "m.room.member", state_key: userID })
|
this.#stateRequests.push({ room_id: room.roomID, type: "m.room.member", state_key: userID })
|
||||||
if (!this.#stateRequestQueued) {
|
if (this.#stateRequestPromise === null) {
|
||||||
this.#stateRequestQueued = true
|
this.#stateRequestPromise = new Promise(this.#doStateRequestsPromise)
|
||||||
window.queueMicrotask(this.doStateRequests)
|
|
||||||
}
|
}
|
||||||
|
return this.#stateRequestPromise
|
||||||
}
|
}
|
||||||
|
|
||||||
doStateRequests = () => {
|
#doStateRequestsPromise = (resolve: () => void) => {
|
||||||
const reqs = this.#stateRequests
|
window.queueMicrotask(() => {
|
||||||
this.#stateRequestQueued = false
|
const reqs = this.#stateRequests
|
||||||
this.#stateRequests = []
|
this.#stateRequestPromise = null
|
||||||
this.loadSpecificRoomState(reqs).catch(err => console.error("Failed to load room state", reqs, err))
|
this.#stateRequests = []
|
||||||
|
this.loadSpecificRoomState(reqs)
|
||||||
|
.catch(err => console.error("Failed to load room state", reqs, err))
|
||||||
|
.finally(resolve)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
requestEvent(room: RoomStateStore | RoomID | undefined, eventID: EventID) {
|
requestEvent(room: RoomStateStore | RoomID | undefined, eventID: EventID) {
|
||||||
|
@ -204,7 +284,9 @@ export default class Client {
|
||||||
throw new Error("Room not found")
|
throw new Error("Room not found")
|
||||||
}
|
}
|
||||||
const dbEvent = await this.rpc.sendMessage(params)
|
const dbEvent = await this.rpc.sendMessage(params)
|
||||||
this.#handleOutgoingEvent(dbEvent, room)
|
if (dbEvent) {
|
||||||
|
this.#handleOutgoingEvent(dbEvent, room)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async subscribeToEmojiPack(pack: RoomStateGUID, subscribe: boolean = true) {
|
async subscribeToEmojiPack(pack: RoomStateGUID, subscribe: boolean = true) {
|
||||||
|
@ -314,7 +396,7 @@ export default class Client {
|
||||||
throw new Error("Timeline changed while loading history")
|
throw new Error("Timeline changed while loading history")
|
||||||
}
|
}
|
||||||
room.hasMoreHistory = resp.has_more
|
room.hasMoreHistory = resp.has_more
|
||||||
room.applyPagination(resp.events)
|
room.applyPagination(resp.events, resp.related_events, resp.receipts)
|
||||||
} finally {
|
} finally {
|
||||||
room.paginating = false
|
room.paginating = false
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,9 +13,8 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import type { RoomListEntry } from "@/api/statestore"
|
|
||||||
import { parseMXC } from "@/util/validation.ts"
|
import { parseMXC } from "@/util/validation.ts"
|
||||||
import { ContentURI, DBRoom, UserID, UserProfile } from "./types"
|
import { ContentURI, RoomID, UserID, UserProfile } from "./types"
|
||||||
|
|
||||||
export const getMediaURL = (mxc?: string, encrypted: boolean = false): string | undefined => {
|
export const getMediaURL = (mxc?: string, encrypted: boolean = false): string | undefined => {
|
||||||
const [server, mediaID] = parseMXC(mxc)
|
const [server, mediaID] = parseMXC(mxc)
|
||||||
|
@ -55,7 +54,7 @@ export const getUserColor = (userID: UserID) => {
|
||||||
// note: this should stay in sync with fallbackAvatarTemplate in cmd/gomuks.media.go
|
// note: this should stay in sync with fallbackAvatarTemplate in cmd/gomuks.media.go
|
||||||
function makeFallbackAvatar(backgroundColor: string, fallbackCharacter: string): string {
|
function makeFallbackAvatar(backgroundColor: string, fallbackCharacter: string): string {
|
||||||
return "data:image/svg+xml," + encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000">
|
return "data:image/svg+xml," + encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000">
|
||||||
<circle cx="500" cy="500" r="500" fill="${backgroundColor}"/>
|
<rect x="0" y="0" width="1000" height="1000" fill="${backgroundColor}"/>
|
||||||
<text x="500" y="750" text-anchor="middle" fill="#fff" font-weight="bold" font-size="666"
|
<text x="500" y="750" text-anchor="middle" fill="#fff" font-weight="bold" font-size="666"
|
||||||
font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif"
|
font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif"
|
||||||
>${escapeHTMLChar(fallbackCharacter)}</text>
|
>${escapeHTMLChar(fallbackCharacter)}</text>
|
||||||
|
@ -82,21 +81,26 @@ function getFallbackCharacter(from: unknown, idx: number): string {
|
||||||
export const getAvatarURL = (userID: UserID, content?: UserProfile | null): string | undefined => {
|
export const getAvatarURL = (userID: UserID, content?: UserProfile | null): string | undefined => {
|
||||||
const fallbackCharacter = getFallbackCharacter(content?.displayname, 0) || getFallbackCharacter(userID, 1)
|
const fallbackCharacter = getFallbackCharacter(content?.displayname, 0) || getFallbackCharacter(userID, 1)
|
||||||
const backgroundColor = getUserColor(userID)
|
const backgroundColor = getUserColor(userID)
|
||||||
const [server, mediaID] = parseMXC(content?.avatar_url)
|
const [server, mediaID] = parseMXC(content?.avatar_file?.url ?? content?.avatar_url)
|
||||||
if (!mediaID) {
|
if (!mediaID) {
|
||||||
return makeFallbackAvatar(backgroundColor, fallbackCharacter)
|
return makeFallbackAvatar(backgroundColor, fallbackCharacter)
|
||||||
}
|
}
|
||||||
|
const encrypted = !!content?.avatar_file
|
||||||
const fallback = `${backgroundColor}:${fallbackCharacter}`
|
const fallback = `${backgroundColor}:${fallbackCharacter}`
|
||||||
return `_gomuks/media/${server}/${mediaID}?encrypted=false&fallback=${encodeURIComponent(fallback)}`
|
return `_gomuks/media/${server}/${mediaID}?encrypted=${encrypted}&fallback=${encodeURIComponent(fallback)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getRoomAvatarURL = (room: DBRoom | RoomListEntry, avatarOverride?: ContentURI): string | undefined => {
|
interface RoomForAvatarURL {
|
||||||
let dmUserID: UserID | undefined
|
room_id: RoomID
|
||||||
if ("dm_user_id" in room) {
|
name?: string
|
||||||
dmUserID = room.dm_user_id
|
dm_user_id?: UserID
|
||||||
} else if ("lazy_load_summary" in room) {
|
avatar?: ContentURI
|
||||||
dmUserID = room.lazy_load_summary?.heroes?.length === 1
|
avatar_url?: ContentURI
|
||||||
? room.lazy_load_summary.heroes[0] : undefined
|
}
|
||||||
}
|
|
||||||
return getAvatarURL(dmUserID ?? room.room_id, { displayname: room.name, avatar_url: avatarOverride ?? room.avatar })
|
export const getRoomAvatarURL = (room: RoomForAvatarURL, avatarOverride?: ContentURI): string | undefined => {
|
||||||
|
return getAvatarURL(room.dm_user_id ?? room.room_id, {
|
||||||
|
displayname: room.name,
|
||||||
|
avatar_url: avatarOverride ?? room.avatar ?? room.avatar_url,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,9 +17,11 @@ import { CachedEventDispatcher, EventDispatcher } from "../util/eventdispatcher.
|
||||||
import { CancellablePromise } from "../util/promise.ts"
|
import { CancellablePromise } from "../util/promise.ts"
|
||||||
import type {
|
import type {
|
||||||
ClientWellKnown,
|
ClientWellKnown,
|
||||||
|
DBPushRegistration,
|
||||||
EventID,
|
EventID,
|
||||||
EventRowID,
|
EventRowID,
|
||||||
EventType,
|
EventType,
|
||||||
|
JSONValue,
|
||||||
LoginFlowsResponse,
|
LoginFlowsResponse,
|
||||||
LoginRequest,
|
LoginRequest,
|
||||||
Mentions,
|
Mentions,
|
||||||
|
@ -32,9 +34,12 @@ import type {
|
||||||
ReceiptType,
|
ReceiptType,
|
||||||
RelatesTo,
|
RelatesTo,
|
||||||
ResolveAliasResponse,
|
ResolveAliasResponse,
|
||||||
|
RespOpenIDToken,
|
||||||
|
RespRoomJoin,
|
||||||
RoomAlias,
|
RoomAlias,
|
||||||
RoomID,
|
RoomID,
|
||||||
RoomStateGUID,
|
RoomStateGUID,
|
||||||
|
RoomSummary,
|
||||||
TimelineRowID,
|
TimelineRowID,
|
||||||
UserID,
|
UserID,
|
||||||
UserProfile,
|
UserProfile,
|
||||||
|
@ -136,7 +141,7 @@ export default abstract class RPCClient {
|
||||||
return this.request("logout", {})
|
return this.request("logout", {})
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMessage(params: SendMessageParams): Promise<RawDBEvent> {
|
sendMessage(params: SendMessageParams): Promise<RawDBEvent | null> {
|
||||||
return this.request("send_message", params)
|
return this.request("send_message", params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,6 +183,10 @@ export default abstract class RPCClient {
|
||||||
return this.request("get_profile", { user_id })
|
return this.request("get_profile", { user_id })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setProfileField(field: string, value: JSONValue): Promise<boolean> {
|
||||||
|
return this.request("set_profile_field", { field, value })
|
||||||
|
}
|
||||||
|
|
||||||
getMutualRooms(user_id: UserID): Promise<RoomID[]> {
|
getMutualRooms(user_id: UserID): Promise<RoomID[]> {
|
||||||
return this.request("get_mutual_rooms", { user_id })
|
return this.request("get_mutual_rooms", { user_id })
|
||||||
}
|
}
|
||||||
|
@ -186,6 +195,10 @@ export default abstract class RPCClient {
|
||||||
return this.request("get_profile_encryption_info", { user_id })
|
return this.request("get_profile_encryption_info", { user_id })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trackUserDevices(user_id: UserID): Promise<ProfileEncryptionInfo> {
|
||||||
|
return this.request("track_user_devices", { user_id })
|
||||||
|
}
|
||||||
|
|
||||||
ensureGroupSessionShared(room_id: RoomID): Promise<boolean> {
|
ensureGroupSessionShared(room_id: RoomID): Promise<boolean> {
|
||||||
return this.request("ensure_group_session_shared", { room_id })
|
return this.request("ensure_group_session_shared", { room_id })
|
||||||
}
|
}
|
||||||
|
@ -216,6 +229,18 @@ export default abstract class RPCClient {
|
||||||
return this.request("paginate_server", { room_id, limit })
|
return this.request("paginate_server", { room_id, limit })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getRoomSummary(room_id_or_alias: RoomID | RoomAlias, via?: string[]): Promise<RoomSummary> {
|
||||||
|
return this.request("get_room_summary", { room_id_or_alias, via })
|
||||||
|
}
|
||||||
|
|
||||||
|
joinRoom(room_id_or_alias: RoomID | RoomAlias, via?: string[], reason?: string): Promise<RespRoomJoin> {
|
||||||
|
return this.request("join_room", { room_id_or_alias, via, reason })
|
||||||
|
}
|
||||||
|
|
||||||
|
leaveRoom(room_id: RoomID, reason?: string): Promise<Record<string, never>> {
|
||||||
|
return this.request("leave_room", { room_id, reason })
|
||||||
|
}
|
||||||
|
|
||||||
resolveAlias(alias: RoomAlias): Promise<ResolveAliasResponse> {
|
resolveAlias(alias: RoomAlias): Promise<ResolveAliasResponse> {
|
||||||
return this.request("resolve_alias", { alias })
|
return this.request("resolve_alias", { alias })
|
||||||
}
|
}
|
||||||
|
@ -239,4 +264,12 @@ export default abstract class RPCClient {
|
||||||
verify(recovery_key: string): Promise<boolean> {
|
verify(recovery_key: string): Promise<boolean> {
|
||||||
return this.request("verify", { recovery_key })
|
return this.request("verify", { recovery_key })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
requestOpenIDToken(): Promise<RespOpenIDToken> {
|
||||||
|
return this.request("request_openid_token", {})
|
||||||
|
}
|
||||||
|
|
||||||
|
registerPush(reg: DBPushRegistration): Promise<boolean> {
|
||||||
|
return this.request("register_push", reg)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,9 +13,18 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import { useEffect, useMemo, useState, useSyncExternalStore } from "react"
|
import { useEffect, useMemo, useReducer, useState, useSyncExternalStore } from "react"
|
||||||
|
import Client from "@/api/client.ts"
|
||||||
import type { CustomEmojiPack } from "@/util/emoji"
|
import type { CustomEmojiPack } from "@/util/emoji"
|
||||||
import type { EventID, EventType, MemDBEvent, UnknownEventContent } from "../types"
|
import type {
|
||||||
|
EventID,
|
||||||
|
EventType,
|
||||||
|
MemDBEvent,
|
||||||
|
MemReceipt,
|
||||||
|
MemberEventContent,
|
||||||
|
UnknownEventContent,
|
||||||
|
UserID,
|
||||||
|
} from "../types"
|
||||||
import { Preferences, preferences } from "../types/preferences"
|
import { Preferences, preferences } from "../types/preferences"
|
||||||
import type { StateStore } from "./main.ts"
|
import type { StateStore } from "./main.ts"
|
||||||
import type { AutocompleteMemberEntry, RoomStateStore } from "./room.ts"
|
import type { AutocompleteMemberEntry, RoomStateStore } from "./room.ts"
|
||||||
|
@ -27,6 +36,17 @@ export function useRoomTimeline(room: RoomStateStore): (MemDBEvent | null)[] {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useRoomTyping(room: RoomStateStore): string[] {
|
||||||
|
return useSyncExternalStore(room.typingSub.subscribe, () => room.typing)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useReadReceipts(room: RoomStateStore, evtID: EventID): MemReceipt[] {
|
||||||
|
return useSyncExternalStore(
|
||||||
|
room.receiptSubs.getSubscriber(evtID),
|
||||||
|
() => room.receiptsByEventID.get(evtID) ?? emptyArray,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function useRoomState(
|
export function useRoomState(
|
||||||
room?: RoomStateStore, type?: EventType, stateKey: string | undefined = "",
|
room?: RoomStateStore, type?: EventType, stateKey: string | undefined = "",
|
||||||
): MemDBEvent | null {
|
): MemDBEvent | null {
|
||||||
|
@ -37,6 +57,34 @@ export function useRoomState(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useRoomMember(
|
||||||
|
client: Client | undefined | null, room: RoomStateStore | undefined, userID: UserID,
|
||||||
|
): MemDBEvent | null {
|
||||||
|
const evt = useRoomState(room, "m.room.member", userID)
|
||||||
|
if (!evt && client && room) {
|
||||||
|
client.requestMemberEvent(room, userID)
|
||||||
|
}
|
||||||
|
return evt
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMultipleRoomMembers(
|
||||||
|
client: Client, room: RoomStateStore, userIDs: UserID[],
|
||||||
|
): [UserID, MemberEventContent | null][] {
|
||||||
|
const [, forceUpdate] = useReducer(x => x + 1, 0)
|
||||||
|
let promiseAwaited = false
|
||||||
|
return userIDs.map(userID => {
|
||||||
|
const evt = room.getStateEvent("m.room.member", userID)
|
||||||
|
if (!evt) {
|
||||||
|
const promise = client.requestMemberEvent(room, userID)
|
||||||
|
if (promise && !promiseAwaited) {
|
||||||
|
promiseAwaited = true
|
||||||
|
promise.then(forceUpdate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const member = (evt?.content ?? null) as MemberEventContent | null
|
||||||
|
return [userID, member]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function useRoomMembers(room?: RoomStateStore): AutocompleteMemberEntry[] {
|
export function useRoomMembers(room?: RoomStateStore): AutocompleteMemberEntry[] {
|
||||||
return useSyncExternalStore(
|
return useSyncExternalStore(
|
||||||
|
@ -97,7 +145,7 @@ export function usePreference<T extends keyof Preferences>(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCustomEmojis(
|
export function useCustomEmojis(
|
||||||
ss: StateStore, room: RoomStateStore,
|
ss: StateStore, room: RoomStateStore, usage: "stickers" | "emojis" = "emojis",
|
||||||
): CustomEmojiPack[] {
|
): CustomEmojiPack[] {
|
||||||
const personalPack = useSyncExternalStore(
|
const personalPack = useSyncExternalStore(
|
||||||
ss.accountDataSubs.getSubscriber("im.ponies.user_emotes"),
|
ss.accountDataSubs.getSubscriber("im.ponies.user_emotes"),
|
||||||
|
@ -116,6 +164,6 @@ export function useCustomEmojis(
|
||||||
if (personalPack) {
|
if (personalPack) {
|
||||||
allPacksObject.personal = personalPack
|
allPacksObject.personal = personalPack
|
||||||
}
|
}
|
||||||
return Object.values(allPacksObject)
|
return Object.values(allPacksObject).filter(pack => pack[usage].length > 0)
|
||||||
}, [personalPack, watchedRoomPacks, specialRoomPacks])
|
}, [personalPack, watchedRoomPacks, specialRoomPacks, usage])
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
export * from "./main.ts"
|
export * from "./main.ts"
|
||||||
export * from "./room.ts"
|
export * from "./room.ts"
|
||||||
export * from "./hooks.ts"
|
export * from "./hooks.ts"
|
||||||
|
export * from "./space.ts"
|
||||||
|
|
135
web/src/api/statestore/invitedroom.ts
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
// gomuks - A Matrix client written in Go.
|
||||||
|
// Copyright (C) 2024 Tulir Asokan
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
import toSearchableString from "@/util/searchablestring.ts"
|
||||||
|
import { ensureString, getDisplayname } from "@/util/validation.ts"
|
||||||
|
import type {
|
||||||
|
ContentURI,
|
||||||
|
DBInvitedRoom, JoinRule,
|
||||||
|
MemberEventContent, Membership,
|
||||||
|
RoomAlias,
|
||||||
|
RoomID,
|
||||||
|
RoomSummary,
|
||||||
|
RoomType,
|
||||||
|
RoomVersion,
|
||||||
|
StrippedStateEvent,
|
||||||
|
UserID,
|
||||||
|
} from "../types"
|
||||||
|
import type { RoomListEntry, StateStore } from "./main.ts"
|
||||||
|
|
||||||
|
export class InvitedRoomStore implements RoomListEntry, RoomSummary {
|
||||||
|
readonly room_id: RoomID
|
||||||
|
readonly sorting_timestamp: number
|
||||||
|
readonly date: string
|
||||||
|
readonly name: string = ""
|
||||||
|
readonly search_name: string
|
||||||
|
readonly dm_user_id?: UserID
|
||||||
|
readonly canonical_alias?: RoomAlias
|
||||||
|
readonly topic?: string
|
||||||
|
readonly avatar?: ContentURI
|
||||||
|
readonly encryption?: "m.megolm.v1.aes-sha2"
|
||||||
|
readonly room_version?: RoomVersion
|
||||||
|
readonly join_rule?: JoinRule
|
||||||
|
readonly invited_by?: UserID
|
||||||
|
readonly inviter_profile?: MemberEventContent
|
||||||
|
readonly is_direct: boolean
|
||||||
|
|
||||||
|
constructor(public readonly meta: DBInvitedRoom, parent: StateStore) {
|
||||||
|
this.room_id = meta.room_id
|
||||||
|
this.sorting_timestamp = 1000000000000000 + meta.created_at
|
||||||
|
this.date = new Date(meta.created_at - new Date().getTimezoneOffset() * 60000)
|
||||||
|
.toISOString().replace("T", " ").replace("Z", "")
|
||||||
|
const members = new Map<UserID, StrippedStateEvent>()
|
||||||
|
for (const state of this.meta.invite_state) {
|
||||||
|
if (state.type === "m.room.name") {
|
||||||
|
this.name = ensureString(state.content.name)
|
||||||
|
} else if (state.type === "m.room.canonical_alias") {
|
||||||
|
this.canonical_alias = ensureString(state.content.alias)
|
||||||
|
} else if (state.type === "m.room.topic") {
|
||||||
|
this.topic = ensureString(state.content.topic)
|
||||||
|
} else if (state.type === "m.room.avatar") {
|
||||||
|
this.avatar = ensureString(state.content.url)
|
||||||
|
} else if (state.type === "m.room.encryption") {
|
||||||
|
this.encryption = state.content.algorithm as "m.megolm.v1.aes-sha2"
|
||||||
|
} else if (state.type === "m.room.create") {
|
||||||
|
this.room_version = ensureString(state.content.version) as RoomVersion
|
||||||
|
} else if (state.type === "m.room.member") {
|
||||||
|
members.set(state.state_key, state)
|
||||||
|
} else if (state.type === "m.room.join_rules") {
|
||||||
|
this.join_rule = ensureString(state.content.join_rule) as JoinRule
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.search_name = toSearchableString(this.name ?? "")
|
||||||
|
const ownMemberEvt = members.get(parent.userID)
|
||||||
|
if (ownMemberEvt) {
|
||||||
|
this.invited_by = ownMemberEvt.sender
|
||||||
|
this.inviter_profile = members.get(ownMemberEvt.sender)?.content as MemberEventContent
|
||||||
|
}
|
||||||
|
this.is_direct = Boolean(ownMemberEvt?.content.is_direct)
|
||||||
|
if (
|
||||||
|
!this.name
|
||||||
|
&& !this.avatar
|
||||||
|
&& !this.topic
|
||||||
|
&& !this.canonical_alias
|
||||||
|
&& this.join_rule === "invite"
|
||||||
|
&& this.invited_by
|
||||||
|
&& this.is_direct
|
||||||
|
) {
|
||||||
|
this.dm_user_id = this.invited_by
|
||||||
|
this.name = getDisplayname(this.invited_by, this.inviter_profile)
|
||||||
|
this.avatar = this.inviter_profile?.avatar_url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get membership(): Membership {
|
||||||
|
return "invite"
|
||||||
|
}
|
||||||
|
|
||||||
|
get avatar_url(): ContentURI | undefined {
|
||||||
|
return this.avatar
|
||||||
|
}
|
||||||
|
|
||||||
|
get num_joined_members(): number {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
get room_type(): RoomType {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
get world_readable(): boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
get guest_can_join(): boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
get unread_messages(): number {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
get unread_notifications(): number {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
get unread_highlights(): number {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
get marked_unread(): boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,11 +32,14 @@ import {
|
||||||
SendCompleteData,
|
SendCompleteData,
|
||||||
SyncCompleteData,
|
SyncCompleteData,
|
||||||
SyncRoom,
|
SyncRoom,
|
||||||
|
TypingEventData,
|
||||||
UnknownEventContent,
|
UnknownEventContent,
|
||||||
UserID,
|
UserID,
|
||||||
roomStateGUIDToString,
|
roomStateGUIDToString,
|
||||||
} from "../types"
|
} from "../types"
|
||||||
|
import { InvitedRoomStore } from "./invitedroom.ts"
|
||||||
import { RoomStateStore } from "./room.ts"
|
import { RoomStateStore } from "./room.ts"
|
||||||
|
import { DirectChatSpace, RoomListFilter, Space, SpaceEdgeStore, SpaceOrphansSpace, UnreadsSpace } from "./space.ts"
|
||||||
|
|
||||||
export interface RoomListEntry {
|
export interface RoomListEntry {
|
||||||
room_id: RoomID
|
room_id: RoomID
|
||||||
|
@ -66,13 +69,27 @@ window.gcSettings ??= {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StateStore {
|
export class StateStore {
|
||||||
|
userID: UserID = ""
|
||||||
readonly rooms: Map<RoomID, RoomStateStore> = new Map()
|
readonly rooms: Map<RoomID, RoomStateStore> = new Map()
|
||||||
|
readonly inviteRooms: Map<RoomID, InvitedRoomStore> = new Map()
|
||||||
readonly roomList = new NonNullCachedEventDispatcher<RoomListEntry[]>([])
|
readonly roomList = new NonNullCachedEventDispatcher<RoomListEntry[]>([])
|
||||||
currentRoomListFilter: string = ""
|
readonly roomListEntries = new Map<RoomID, RoomListEntry>()
|
||||||
|
readonly topLevelSpaces = new NonNullCachedEventDispatcher<RoomID[]>([])
|
||||||
|
readonly spaceEdges: Map<RoomID, SpaceEdgeStore> = new Map()
|
||||||
|
readonly spaceOrphans = new SpaceOrphansSpace(this)
|
||||||
|
readonly directChatsSpace = new DirectChatSpace()
|
||||||
|
readonly unreadsSpace = new UnreadsSpace(this)
|
||||||
|
readonly pseudoSpaces = [
|
||||||
|
this.spaceOrphans,
|
||||||
|
this.directChatsSpace,
|
||||||
|
this.unreadsSpace,
|
||||||
|
] as const
|
||||||
|
currentRoomListQuery: string = ""
|
||||||
|
currentRoomListFilter: RoomListFilter | null = null
|
||||||
readonly accountData: Map<string, UnknownEventContent> = new Map()
|
readonly accountData: Map<string, UnknownEventContent> = new Map()
|
||||||
readonly accountDataSubs = new MultiSubscribable()
|
readonly accountDataSubs = new MultiSubscribable()
|
||||||
readonly emojiRoomsSub = new Subscribable()
|
readonly emojiRoomsSub = new Subscribable()
|
||||||
readonly preferences: Preferences = getPreferenceProxy(this)
|
readonly preferences = getPreferenceProxy(this)
|
||||||
#frequentlyUsedEmoji: Map<string, number> | null = null
|
#frequentlyUsedEmoji: Map<string, number> | null = null
|
||||||
#emojiPackKeys: RoomStateGUID[] | null = null
|
#emojiPackKeys: RoomStateGUID[] | null = null
|
||||||
#watchedRoomEmojiPacks: Record<string, CustomEmojiPack> | null = null
|
#watchedRoomEmojiPacks: Record<string, CustomEmojiPack> | null = null
|
||||||
|
@ -82,13 +99,61 @@ export class StateStore {
|
||||||
serverPreferenceCache: Preferences = {}
|
serverPreferenceCache: Preferences = {}
|
||||||
switchRoom?: (roomID: RoomID | null) => void
|
switchRoom?: (roomID: RoomID | null) => void
|
||||||
activeRoomID: RoomID | null = null
|
activeRoomID: RoomID | null = null
|
||||||
|
activeRoomIsPreview: boolean = false
|
||||||
imageAuthToken?: string
|
imageAuthToken?: string
|
||||||
|
|
||||||
getFilteredRoomList(): RoomListEntry[] {
|
#roomListFilterFunc = (entry: RoomListEntry) => {
|
||||||
if (!this.currentRoomListFilter) {
|
if (this.currentRoomListQuery && !entry.search_name.includes(this.currentRoomListQuery)) {
|
||||||
return this.roomList.current
|
return false
|
||||||
|
} else if (this.currentRoomListFilter && !this.currentRoomListFilter.include(entry)) {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
return this.roomList.current.filter(entry => entry.search_name.includes(this.currentRoomListFilter))
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
getSpaceByID(spaceID: string | undefined): RoomListFilter | null {
|
||||||
|
if (!spaceID) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const realSpace = this.spaceEdges.get(spaceID)
|
||||||
|
if (realSpace) {
|
||||||
|
return realSpace
|
||||||
|
}
|
||||||
|
for (const pseudoSpace of this.pseudoSpaces) {
|
||||||
|
if (pseudoSpace.id === spaceID) {
|
||||||
|
return pseudoSpace
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.warn("Failed to find space", spaceID)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
findMatchingSpace(room: RoomListEntry): Space | null {
|
||||||
|
if (this.spaceOrphans.include(room)) {
|
||||||
|
return this.spaceOrphans
|
||||||
|
}
|
||||||
|
for (const spaceID of this.topLevelSpaces.current) {
|
||||||
|
const space = this.spaceEdges.get(spaceID)
|
||||||
|
if (space?.include(room)) {
|
||||||
|
return space
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.directChatsSpace.include(room)) {
|
||||||
|
return this.directChatsSpace
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
get roomListFilterFunc(): ((entry: RoomListEntry) => boolean) | null {
|
||||||
|
if (!this.currentRoomListFilter && !this.currentRoomListQuery) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return this.#roomListFilterFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
getFilteredRoomList(): RoomListEntry[] {
|
||||||
|
const fn = this.roomListFilterFunc
|
||||||
|
return fn ? this.roomList.current.filter(fn) : this.roomList.current
|
||||||
}
|
}
|
||||||
|
|
||||||
#shouldHideRoom(entry: SyncRoom): boolean {
|
#shouldHideRoom(entry: SyncRoom): boolean {
|
||||||
|
@ -117,7 +182,7 @@ export class StateStore {
|
||||||
entry.meta.unread_highlights !== oldEntry.meta.current.unread_highlights ||
|
entry.meta.unread_highlights !== oldEntry.meta.current.unread_highlights ||
|
||||||
entry.meta.marked_unread !== oldEntry.meta.current.marked_unread ||
|
entry.meta.marked_unread !== oldEntry.meta.current.marked_unread ||
|
||||||
entry.meta.preview_event_rowid !== oldEntry.meta.current.preview_event_rowid ||
|
entry.meta.preview_event_rowid !== oldEntry.meta.current.preview_event_rowid ||
|
||||||
entry.events.findIndex(evt => evt.rowid === entry.meta.preview_event_rowid) !== -1
|
(entry.events ?? []).findIndex(evt => evt.rowid === entry.meta.preview_event_rowid) !== -1
|
||||||
}
|
}
|
||||||
|
|
||||||
#makeRoomListEntry(entry: SyncRoom, room?: RoomStateStore): RoomListEntry | null {
|
#makeRoomListEntry(entry: SyncRoom, room?: RoomStateStore): RoomListEntry | null {
|
||||||
|
@ -138,8 +203,7 @@ export class StateStore {
|
||||||
const name = entry.meta.name ?? "Unnamed room"
|
const name = entry.meta.name ?? "Unnamed room"
|
||||||
return {
|
return {
|
||||||
room_id: entry.meta.room_id,
|
room_id: entry.meta.room_id,
|
||||||
dm_user_id: entry.meta.lazy_load_summary?.heroes?.length === 1
|
dm_user_id: entry.meta.dm_user_id,
|
||||||
? entry.meta.lazy_load_summary.heroes[0] : undefined,
|
|
||||||
sorting_timestamp: entry.meta.sorting_timestamp,
|
sorting_timestamp: entry.meta.sorting_timestamp,
|
||||||
preview_event,
|
preview_event,
|
||||||
preview_sender,
|
preview_sender,
|
||||||
|
@ -153,6 +217,25 @@ export class StateStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#applyUnreadModification(meta: RoomListEntry | null, oldMeta: RoomListEntry | undefined | null) {
|
||||||
|
const someMeta = meta ?? oldMeta
|
||||||
|
if (!someMeta) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.spaceOrphans.include(someMeta)) {
|
||||||
|
this.spaceOrphans.applyUnreads(meta, oldMeta)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.directChatsSpace.include(someMeta)) {
|
||||||
|
this.directChatsSpace.applyUnreads(meta, oldMeta)
|
||||||
|
}
|
||||||
|
for (const space of this.spaceEdges.values()) {
|
||||||
|
if (space.include(someMeta)) {
|
||||||
|
space.applyUnreads(meta, oldMeta)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
applySync(sync: SyncCompleteData) {
|
applySync(sync: SyncCompleteData) {
|
||||||
if (sync.clear_state && this.rooms.size > 0) {
|
if (sync.clear_state && this.rooms.size > 0) {
|
||||||
console.info("Clearing state store as sync told to reset and there are rooms in the store")
|
console.info("Clearing state store as sync told to reset and there are rooms in the store")
|
||||||
|
@ -160,18 +243,41 @@ export class StateStore {
|
||||||
}
|
}
|
||||||
const resyncRoomList = this.roomList.current.length === 0
|
const resyncRoomList = this.roomList.current.length === 0
|
||||||
const changedRoomListEntries = new Map<RoomID, RoomListEntry | null>()
|
const changedRoomListEntries = new Map<RoomID, RoomListEntry | null>()
|
||||||
for (const [roomID, data] of Object.entries(sync.rooms)) {
|
for (const data of sync.invited_rooms ?? []) {
|
||||||
|
const room = new InvitedRoomStore(data, this)
|
||||||
|
this.inviteRooms.set(room.room_id, room)
|
||||||
|
if (!resyncRoomList) {
|
||||||
|
changedRoomListEntries.set(room.room_id, room)
|
||||||
|
this.#applyUnreadModification(room, this.roomListEntries.get(room.room_id))
|
||||||
|
this.roomListEntries.set(room.room_id, room)
|
||||||
|
}
|
||||||
|
if (this.activeRoomID === room.room_id) {
|
||||||
|
this.switchRoom?.(room.room_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const hasInvites = this.inviteRooms.size > 0
|
||||||
|
for (const [roomID, data] of Object.entries(sync.rooms ?? {})) {
|
||||||
let isNewRoom = false
|
let isNewRoom = false
|
||||||
let room = this.rooms.get(roomID)
|
let room = this.rooms.get(roomID)
|
||||||
if (!room) {
|
if (!room) {
|
||||||
room = new RoomStateStore(data.meta, this)
|
room = new RoomStateStore(data.meta, this)
|
||||||
this.rooms.set(roomID, room)
|
this.rooms.set(roomID, room)
|
||||||
|
if (hasInvites) {
|
||||||
|
this.inviteRooms.delete(roomID)
|
||||||
|
}
|
||||||
isNewRoom = true
|
isNewRoom = true
|
||||||
}
|
}
|
||||||
const roomListEntryChanged = !resyncRoomList && (isNewRoom || this.#roomListEntryChanged(data, room))
|
const roomListEntryChanged = !resyncRoomList && (isNewRoom || this.#roomListEntryChanged(data, room))
|
||||||
room.applySync(data)
|
room.applySync(data)
|
||||||
if (roomListEntryChanged) {
|
if (roomListEntryChanged) {
|
||||||
changedRoomListEntries.set(roomID, this.#makeRoomListEntry(data, room))
|
const entry = this.#makeRoomListEntry(data, room)
|
||||||
|
changedRoomListEntries.set(roomID, entry)
|
||||||
|
this.#applyUnreadModification(entry, this.roomListEntries.get(roomID))
|
||||||
|
if (entry) {
|
||||||
|
this.roomListEntries.set(roomID, entry)
|
||||||
|
} else {
|
||||||
|
this.roomListEntries.delete(roomID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!resyncRoomList) {
|
if (!resyncRoomList) {
|
||||||
// When we join a valid replacement room, hide the tombstoned room.
|
// When we join a valid replacement room, hide the tombstoned room.
|
||||||
|
@ -184,13 +290,16 @@ export class StateStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.Notification?.permission === "granted" && !focused.current) {
|
if (window.Notification?.permission === "granted" && !focused.current && data.notifications) {
|
||||||
for (const notification of data.notifications) {
|
for (const notification of data.notifications) {
|
||||||
this.showNotification(room, notification.event_rowid, notification.sound)
|
this.showNotification(room, notification.event_rowid, notification.sound)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (this.activeRoomID === roomID && this.activeRoomIsPreview) {
|
||||||
|
this.switchRoom?.(roomID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (const ad of Object.values(sync.account_data)) {
|
for (const ad of Object.values(sync.account_data ?? {})) {
|
||||||
if (ad.type === "io.element.recent_emoji") {
|
if (ad.type === "io.element.recent_emoji") {
|
||||||
this.#frequentlyUsedEmoji = null
|
this.#frequentlyUsedEmoji = null
|
||||||
} else if (ad.type === "fi.mau.gomuks.preferences") {
|
} else if (ad.type === "fi.mau.gomuks.preferences") {
|
||||||
|
@ -200,20 +309,26 @@ export class StateStore {
|
||||||
this.accountData.set(ad.type, ad.content)
|
this.accountData.set(ad.type, ad.content)
|
||||||
this.accountDataSubs.notify(ad.type)
|
this.accountDataSubs.notify(ad.type)
|
||||||
}
|
}
|
||||||
for (const roomID of sync.left_rooms) {
|
for (const roomID of sync.left_rooms ?? []) {
|
||||||
if (this.activeRoomID === roomID) {
|
if (this.activeRoomID === roomID) {
|
||||||
this.switchRoom?.(null)
|
this.switchRoom?.(null)
|
||||||
}
|
}
|
||||||
this.rooms.delete(roomID)
|
this.rooms.delete(roomID)
|
||||||
changedRoomListEntries.set(roomID, null)
|
changedRoomListEntries.set(roomID, null)
|
||||||
|
this.#applyUnreadModification(null, this.roomListEntries.get(roomID))
|
||||||
}
|
}
|
||||||
|
|
||||||
let updatedRoomList: RoomListEntry[] | undefined
|
let updatedRoomList: RoomListEntry[] | undefined
|
||||||
if (resyncRoomList) {
|
if (resyncRoomList) {
|
||||||
updatedRoomList = Object.values(sync.rooms)
|
updatedRoomList = this.inviteRooms.values().toArray()
|
||||||
|
updatedRoomList = updatedRoomList.concat(Object.values(sync.rooms ?? {})
|
||||||
.map(entry => this.#makeRoomListEntry(entry))
|
.map(entry => this.#makeRoomListEntry(entry))
|
||||||
.filter(entry => entry !== null)
|
.filter(entry => entry !== null))
|
||||||
updatedRoomList.sort((r1, r2) => r1.sorting_timestamp - r2.sorting_timestamp)
|
updatedRoomList.sort((r1, r2) => r1.sorting_timestamp - r2.sorting_timestamp)
|
||||||
|
for (const entry of updatedRoomList) {
|
||||||
|
this.#applyUnreadModification(entry, undefined)
|
||||||
|
this.roomListEntries.set(entry.room_id, entry)
|
||||||
|
}
|
||||||
} else if (changedRoomListEntries.size > 0) {
|
} else if (changedRoomListEntries.size > 0) {
|
||||||
updatedRoomList = this.roomList.current.filter(entry => !changedRoomListEntries.has(entry.room_id))
|
updatedRoomList = this.roomList.current.filter(entry => !changedRoomListEntries.has(entry.room_id))
|
||||||
for (const entry of changedRoomListEntries.values()) {
|
for (const entry of changedRoomListEntries.values()) {
|
||||||
|
@ -236,6 +351,19 @@ export class StateStore {
|
||||||
if (updatedRoomList) {
|
if (updatedRoomList) {
|
||||||
this.roomList.emit(updatedRoomList)
|
this.roomList.emit(updatedRoomList)
|
||||||
}
|
}
|
||||||
|
if (sync.space_edges) {
|
||||||
|
// Ensure all space stores exist first
|
||||||
|
for (const spaceID of Object.keys(sync.space_edges)) {
|
||||||
|
this.getSpaceStore(spaceID, true)
|
||||||
|
}
|
||||||
|
for (const [spaceID, children] of Object.entries(sync.space_edges ?? {})) {
|
||||||
|
this.getSpaceStore(spaceID, true).children = children
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sync.top_level_spaces) {
|
||||||
|
this.topLevelSpaces.emit(sync.top_level_spaces)
|
||||||
|
this.spaceOrphans.children = sync.top_level_spaces.map(child_id => ({ child_id }))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
invalidateEmojiPackKeyCache() {
|
invalidateEmojiPackKeyCache() {
|
||||||
|
@ -301,6 +429,20 @@ export class StateStore {
|
||||||
return this.#watchedRoomEmojiPacks ?? {}
|
return this.#watchedRoomEmojiPacks ?? {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSpaceStore(spaceID: RoomID, force: true): SpaceEdgeStore
|
||||||
|
getSpaceStore(spaceID: RoomID): SpaceEdgeStore | null
|
||||||
|
getSpaceStore(spaceID: RoomID, force?: true): SpaceEdgeStore | null {
|
||||||
|
let store = this.spaceEdges.get(spaceID)
|
||||||
|
if (!store) {
|
||||||
|
if (!force && this.rooms.get(spaceID)?.meta.current.creation_content?.type !== "m.space") {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
store = new SpaceEdgeStore(spaceID, this)
|
||||||
|
this.spaceEdges.set(spaceID, store)
|
||||||
|
}
|
||||||
|
return store
|
||||||
|
}
|
||||||
|
|
||||||
get frequentlyUsedEmoji(): Map<string, number> {
|
get frequentlyUsedEmoji(): Map<string, number> {
|
||||||
if (this.#frequentlyUsedEmoji === null) {
|
if (this.#frequentlyUsedEmoji === null) {
|
||||||
const emojiData = this.accountData.get("io.element.recent_emoji")
|
const emojiData = this.accountData.get("io.element.recent_emoji")
|
||||||
|
@ -337,9 +479,10 @@ export class StateStore {
|
||||||
const notif = new Notification(title, {
|
const notif = new Notification(title, {
|
||||||
body,
|
body,
|
||||||
icon,
|
icon,
|
||||||
badge: "/gomuks.png",
|
badge: "gomuks.png",
|
||||||
// timestamp: evt.timestamp,
|
// timestamp: evt.timestamp,
|
||||||
// image: ...,
|
// image: ...,
|
||||||
|
silent: !sound,
|
||||||
tag: rowid.toString(),
|
tag: rowid.toString(),
|
||||||
})
|
})
|
||||||
room.openNotifications.set(rowid, notif)
|
room.openNotifications.set(rowid, notif)
|
||||||
|
@ -382,6 +525,15 @@ export class StateStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyTyping(typing: TypingEventData) {
|
||||||
|
const room = this.rooms.get(typing.room_id)
|
||||||
|
if (!room) {
|
||||||
|
// TODO log or something?
|
||||||
|
return
|
||||||
|
}
|
||||||
|
room.applyTyping(typing.user_ids)
|
||||||
|
}
|
||||||
|
|
||||||
doGarbageCollection() {
|
doGarbageCollection() {
|
||||||
const maxLastOpened = Date.now() - window.gcSettings.lastOpenedCutoff
|
const maxLastOpened = Date.now() - window.gcSettings.lastOpenedCutoff
|
||||||
let deletedEvents = 0
|
let deletedEvents = 0
|
||||||
|
@ -399,9 +551,14 @@ export class StateStore {
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
this.rooms.clear()
|
this.rooms.clear()
|
||||||
|
this.inviteRooms.clear()
|
||||||
|
this.spaceEdges.clear()
|
||||||
|
this.pseudoSpaces.forEach(space => space.clearUnreads())
|
||||||
this.roomList.emit([])
|
this.roomList.emit([])
|
||||||
|
this.topLevelSpaces.emit([])
|
||||||
this.accountData.clear()
|
this.accountData.clear()
|
||||||
this.currentRoomListFilter = ""
|
this.currentRoomListQuery = ""
|
||||||
|
this.currentRoomListFilter = null
|
||||||
this.#frequentlyUsedEmoji = null
|
this.#frequentlyUsedEmoji = null
|
||||||
this.#emojiPackKeys = null
|
this.#emojiPackKeys = null
|
||||||
this.#watchedRoomEmojiPacks = null
|
this.#watchedRoomEmojiPacks = null
|
||||||
|
|
|
@ -21,6 +21,7 @@ import Subscribable, { MultiSubscribable, NoDataSubscribable } from "@/util/subs
|
||||||
import { getDisplayname } from "@/util/validation.ts"
|
import { getDisplayname } from "@/util/validation.ts"
|
||||||
import {
|
import {
|
||||||
ContentURI,
|
ContentURI,
|
||||||
|
DBReceipt,
|
||||||
DBRoom,
|
DBRoom,
|
||||||
EncryptedEventContent,
|
EncryptedEventContent,
|
||||||
EventID,
|
EventID,
|
||||||
|
@ -30,6 +31,7 @@ import {
|
||||||
ImagePack,
|
ImagePack,
|
||||||
LazyLoadSummary,
|
LazyLoadSummary,
|
||||||
MemDBEvent,
|
MemDBEvent,
|
||||||
|
MemReceipt,
|
||||||
MemberEventContent,
|
MemberEventContent,
|
||||||
PowerLevelEventContent,
|
PowerLevelEventContent,
|
||||||
RawDBEvent,
|
RawDBEvent,
|
||||||
|
@ -60,7 +62,7 @@ function arraysAreEqual<T>(arr1?: T[], arr2?: T[]): boolean {
|
||||||
function llSummaryIsEqual(ll1?: LazyLoadSummary, ll2?: LazyLoadSummary): boolean {
|
function llSummaryIsEqual(ll1?: LazyLoadSummary, ll2?: LazyLoadSummary): boolean {
|
||||||
return ll1?.["m.joined_member_count"] === ll2?.["m.joined_member_count"] &&
|
return ll1?.["m.joined_member_count"] === ll2?.["m.joined_member_count"] &&
|
||||||
ll1?.["m.invited_member_count"] === ll2?.["m.invited_member_count"] &&
|
ll1?.["m.invited_member_count"] === ll2?.["m.invited_member_count"] &&
|
||||||
arraysAreEqual(ll1?.heroes, ll2?.heroes)
|
arraysAreEqual(ll1?.["m.heroes"], ll2?.["m.heroes"])
|
||||||
}
|
}
|
||||||
|
|
||||||
function visibleMetaIsEqual(meta1: DBRoom, meta2: DBRoom): boolean {
|
function visibleMetaIsEqual(meta1: DBRoom, meta2: DBRoom): boolean {
|
||||||
|
@ -68,6 +70,7 @@ function visibleMetaIsEqual(meta1: DBRoom, meta2: DBRoom): boolean {
|
||||||
meta1.avatar === meta2.avatar &&
|
meta1.avatar === meta2.avatar &&
|
||||||
meta1.topic === meta2.topic &&
|
meta1.topic === meta2.topic &&
|
||||||
meta1.canonical_alias === meta2.canonical_alias &&
|
meta1.canonical_alias === meta2.canonical_alias &&
|
||||||
|
meta1.dm_user_id === meta2.dm_user_id &&
|
||||||
llSummaryIsEqual(meta1.lazy_load_summary, meta2.lazy_load_summary) &&
|
llSummaryIsEqual(meta1.lazy_load_summary, meta2.lazy_load_summary) &&
|
||||||
meta1.encryption_event?.algorithm === meta2.encryption_event?.algorithm &&
|
meta1.encryption_event?.algorithm === meta2.encryption_event?.algorithm &&
|
||||||
meta1.has_member_list === meta2.has_member_list
|
meta1.has_member_list === meta2.has_member_list
|
||||||
|
@ -83,26 +86,34 @@ export interface AutocompleteMemberEntry {
|
||||||
|
|
||||||
const collator = new Intl.Collator()
|
const collator = new Intl.Collator()
|
||||||
|
|
||||||
|
const UNSENT_TIMELINE_ROWID_BASE = 1000000000000000
|
||||||
|
|
||||||
export class RoomStateStore {
|
export class RoomStateStore {
|
||||||
readonly roomID: RoomID
|
readonly roomID: RoomID
|
||||||
readonly meta: NonNullCachedEventDispatcher<DBRoom>
|
readonly meta: NonNullCachedEventDispatcher<DBRoom>
|
||||||
timeline: TimelineRowTuple[] = []
|
timeline: TimelineRowTuple[] = []
|
||||||
timelineCache: (MemDBEvent | null)[] = []
|
timelineCache: (MemDBEvent | null)[] = []
|
||||||
|
editTargets: EventRowID[] = []
|
||||||
state: Map<EventType, Map<string, EventRowID>> = new Map()
|
state: Map<EventType, Map<string, EventRowID>> = new Map()
|
||||||
stateLoaded = false
|
stateLoaded = false
|
||||||
|
typing: UserID[] = []
|
||||||
fullMembersLoaded = false
|
fullMembersLoaded = false
|
||||||
readonly eventsByRowID: Map<EventRowID, MemDBEvent> = new Map()
|
readonly eventsByRowID: Map<EventRowID, MemDBEvent> = new Map()
|
||||||
readonly eventsByID: Map<EventID, MemDBEvent> = new Map()
|
readonly eventsByID: Map<EventID, MemDBEvent> = new Map()
|
||||||
readonly timelineSub = new Subscribable()
|
readonly timelineSub = new Subscribable()
|
||||||
|
readonly typingSub = new Subscribable()
|
||||||
readonly stateSubs = new MultiSubscribable()
|
readonly stateSubs = new MultiSubscribable()
|
||||||
readonly eventSubs = new MultiSubscribable()
|
readonly eventSubs = new MultiSubscribable()
|
||||||
|
readonly receiptsByEventID: Map<EventID, MemReceipt[]> = new Map()
|
||||||
|
readonly receiptsByUserID: Map<UserID, MemReceipt> = new Map()
|
||||||
|
readonly receiptSubs = new MultiSubscribable()
|
||||||
readonly requestedEvents: Set<EventID> = new Set()
|
readonly requestedEvents: Set<EventID> = new Set()
|
||||||
readonly requestedMembers: Set<UserID> = new Set()
|
readonly requestedMembers: Set<UserID> = new Set()
|
||||||
readonly accountData: Map<string, UnknownEventContent> = new Map()
|
readonly accountData: Map<string, UnknownEventContent> = new Map()
|
||||||
readonly accountDataSubs = new MultiSubscribable()
|
readonly accountDataSubs = new MultiSubscribable()
|
||||||
readonly openNotifications: Map<EventRowID, Notification> = new Map()
|
readonly openNotifications: Map<EventRowID, Notification> = new Map()
|
||||||
readonly #emojiPacksCache: Map<string, CustomEmojiPack | null> = new Map()
|
readonly #emojiPacksCache: Map<string, CustomEmojiPack | null> = new Map()
|
||||||
readonly preferences: Preferences
|
readonly preferences: Required<Preferences>
|
||||||
readonly localPreferenceCache: Preferences
|
readonly localPreferenceCache: Preferences
|
||||||
readonly preferenceSub = new NoDataSubscribable()
|
readonly preferenceSub = new NoDataSubscribable()
|
||||||
serverPreferenceCache: Preferences = {}
|
serverPreferenceCache: Preferences = {}
|
||||||
|
@ -124,17 +135,30 @@ export class RoomStateStore {
|
||||||
this.preferences = getPreferenceProxy(parent, this)
|
this.preferences = getPreferenceProxy(parent, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyTimelineSubscribers() {
|
#updateTimelineCache() {
|
||||||
|
const ownMessages: EventRowID[] = []
|
||||||
this.timelineCache = this.timeline.map(rt => {
|
this.timelineCache = this.timeline.map(rt => {
|
||||||
const evt = this.eventsByRowID.get(rt.event_rowid)
|
const evt = this.eventsByRowID.get(rt.event_rowid)
|
||||||
if (!evt) {
|
if (!evt) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
evt.timeline_rowid = rt.timeline_rowid
|
evt.timeline_rowid = rt.timeline_rowid
|
||||||
|
if (
|
||||||
|
evt.sender === this.parent.userID
|
||||||
|
&& evt.type === "m.room.message"
|
||||||
|
&& evt.relation_type !== "m.replace"
|
||||||
|
) {
|
||||||
|
ownMessages.push(evt.rowid)
|
||||||
|
}
|
||||||
return evt
|
return evt
|
||||||
}).concat(this.pendingEvents
|
}).concat(this.pendingEvents
|
||||||
.map(rowID => this.eventsByRowID.get(rowID))
|
.map(rowID => this.eventsByRowID.get(rowID))
|
||||||
.filter(evt => !!evt))
|
.filter(evt => !!evt))
|
||||||
|
this.editTargets = ownMessages
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyTimelineSubscribers() {
|
||||||
|
this.#updateTimelineCache()
|
||||||
this.timelineSub.notify()
|
this.timelineSub.notify()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -230,15 +254,65 @@ export class RoomStateStore {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
applyPagination(history: RawDBEvent[]) {
|
applyPagination(history: RawDBEvent[], related: RawDBEvent[], allReceipts: Record<EventID, DBReceipt[]>) {
|
||||||
// Pagination comes in newest to oldest, timeline is in the opposite order
|
// Pagination comes in newest to oldest, timeline is in the opposite order
|
||||||
history.reverse()
|
history.reverse()
|
||||||
const newTimeline = history.map(evt => {
|
const newTimeline = history.map(evt => {
|
||||||
this.applyEvent(evt)
|
this.applyEvent(evt)
|
||||||
return { timeline_rowid: evt.timeline_rowid, event_rowid: evt.rowid }
|
return { timeline_rowid: evt.timeline_rowid, event_rowid: evt.rowid }
|
||||||
})
|
})
|
||||||
|
for (const evt of related) {
|
||||||
|
if (!this.eventsByRowID.has(evt.rowid)) {
|
||||||
|
this.applyEvent(evt)
|
||||||
|
}
|
||||||
|
}
|
||||||
this.timeline.splice(0, 0, ...newTimeline)
|
this.timeline.splice(0, 0, ...newTimeline)
|
||||||
this.notifyTimelineSubscribers()
|
this.notifyTimelineSubscribers()
|
||||||
|
for (const [evtID, receipts] of Object.entries(allReceipts)) {
|
||||||
|
this.applyReceipts(receipts, evtID, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applyReceipts(receipts: DBReceipt[], evtID: EventID, override: boolean) {
|
||||||
|
const evt = this.eventsByID.get(evtID)
|
||||||
|
if (!evt?.timeline_rowid) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const filtered = receipts.filter(receipt => this.applyReceipt(receipt, evt))
|
||||||
|
filtered.sort((a, b) => a.timestamp - b.timestamp)
|
||||||
|
if (override) {
|
||||||
|
this.receiptsByEventID.set(evtID, filtered)
|
||||||
|
} else {
|
||||||
|
const existing = this.receiptsByEventID.get(evtID) ?? []
|
||||||
|
this.receiptsByEventID.set(evtID, existing.concat(filtered))
|
||||||
|
}
|
||||||
|
this.receiptSubs.notify(evtID)
|
||||||
|
}
|
||||||
|
|
||||||
|
applyReceipt(receipt: DBReceipt, evt: MemDBEvent): receipt is MemReceipt {
|
||||||
|
const existingReceipt = this.receiptsByUserID.get(receipt.user_id)
|
||||||
|
if (existingReceipt) {
|
||||||
|
if (existingReceipt.timeline_rowid >= evt.timeline_rowid) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const oldArr = this.receiptsByEventID.get(existingReceipt.event_id)
|
||||||
|
if (oldArr) {
|
||||||
|
const updated = oldArr.filter(r => r !== existingReceipt)
|
||||||
|
if (updated.length !== oldArr.length) {
|
||||||
|
if (updated.length === 0) {
|
||||||
|
this.receiptsByEventID.delete(existingReceipt.event_id)
|
||||||
|
} else {
|
||||||
|
this.receiptsByEventID.set(existingReceipt.event_id, updated)
|
||||||
|
}
|
||||||
|
this.receiptSubs.notify(existingReceipt.event_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const memReceipt = receipt as MemReceipt
|
||||||
|
memReceipt.timeline_rowid = evt.timeline_rowid > UNSENT_TIMELINE_ROWID_BASE ? 1 : evt.timeline_rowid
|
||||||
|
memReceipt.event_rowid = evt.rowid
|
||||||
|
this.receiptsByUserID.set(receipt.user_id, memReceipt)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
applyEvent(evt: RawDBEvent, pending: boolean = false) {
|
applyEvent(evt: RawDBEvent, pending: boolean = false) {
|
||||||
|
@ -246,7 +320,7 @@ export class RoomStateStore {
|
||||||
memEvt.mem = true
|
memEvt.mem = true
|
||||||
memEvt.pending = pending
|
memEvt.pending = pending
|
||||||
if (pending) {
|
if (pending) {
|
||||||
memEvt.timeline_rowid = 1000000000000000 + memEvt.timestamp
|
memEvt.timeline_rowid = UNSENT_TIMELINE_ROWID_BASE + memEvt.timestamp
|
||||||
}
|
}
|
||||||
if (evt.type === "m.room.encrypted" && evt.decrypted && evt.decrypted_type) {
|
if (evt.type === "m.room.encrypted" && evt.decrypted && evt.decrypted_type) {
|
||||||
memEvt.type = evt.decrypted_type
|
memEvt.type = evt.decrypted_type
|
||||||
|
@ -285,6 +359,7 @@ export class RoomStateStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.eventSubs.notify(memEvt.event_id)
|
this.eventSubs.notify(memEvt.event_id)
|
||||||
|
return memEvt
|
||||||
}
|
}
|
||||||
|
|
||||||
applySendComplete(evt: RawDBEvent) {
|
applySendComplete(evt: RawDBEvent) {
|
||||||
|
@ -316,7 +391,7 @@ export class RoomStateStore {
|
||||||
} else {
|
} else {
|
||||||
this.meta.emit(sync.meta)
|
this.meta.emit(sync.meta)
|
||||||
}
|
}
|
||||||
for (const ad of Object.values(sync.account_data)) {
|
for (const ad of Object.values(sync.account_data ?? {})) {
|
||||||
if (ad.type === "fi.mau.gomuks.preferences") {
|
if (ad.type === "fi.mau.gomuks.preferences") {
|
||||||
this.serverPreferenceCache = ad.content
|
this.serverPreferenceCache = ad.content
|
||||||
this.preferenceSub.notify()
|
this.preferenceSub.notify()
|
||||||
|
@ -324,10 +399,10 @@ export class RoomStateStore {
|
||||||
this.accountData.set(ad.type, ad.content)
|
this.accountData.set(ad.type, ad.content)
|
||||||
this.accountDataSubs.notify(ad.type)
|
this.accountDataSubs.notify(ad.type)
|
||||||
}
|
}
|
||||||
for (const evt of sync.events) {
|
for (const evt of sync.events ?? []) {
|
||||||
this.applyEvent(evt)
|
this.applyEvent(evt)
|
||||||
}
|
}
|
||||||
for (const [evtType, changedEvts] of Object.entries(sync.state)) {
|
for (const [evtType, changedEvts] of Object.entries(sync.state ?? {})) {
|
||||||
let stateMap = this.state.get(evtType)
|
let stateMap = this.state.get(evtType)
|
||||||
if (!stateMap) {
|
if (!stateMap) {
|
||||||
stateMap = new Map()
|
stateMap = new Map()
|
||||||
|
@ -340,9 +415,9 @@ export class RoomStateStore {
|
||||||
this.stateSubs.notify(evtType)
|
this.stateSubs.notify(evtType)
|
||||||
}
|
}
|
||||||
if (sync.reset) {
|
if (sync.reset) {
|
||||||
this.timeline = sync.timeline
|
this.timeline = sync.timeline ?? []
|
||||||
this.pendingEvents.splice(0, this.pendingEvents.length)
|
this.pendingEvents.splice(0, this.pendingEvents.length)
|
||||||
} else {
|
} else if (sync.timeline) {
|
||||||
this.timeline.push(...sync.timeline)
|
this.timeline.push(...sync.timeline)
|
||||||
}
|
}
|
||||||
if (sync.meta.unread_notifications === 0 && sync.meta.unread_highlights === 0) {
|
if (sync.meta.unread_notifications === 0 && sync.meta.unread_highlights === 0) {
|
||||||
|
@ -352,6 +427,9 @@ export class RoomStateStore {
|
||||||
this.openNotifications.clear()
|
this.openNotifications.clear()
|
||||||
}
|
}
|
||||||
this.notifyTimelineSubscribers()
|
this.notifyTimelineSubscribers()
|
||||||
|
for (const [evtID, receipts] of Object.entries(sync.receipts ?? {})) {
|
||||||
|
this.applyReceipts(receipts, evtID, false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
applyState(evt: RawDBEvent) {
|
applyState(evt: RawDBEvent) {
|
||||||
|
@ -418,6 +496,11 @@ export class RoomStateStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyTyping(users: string[]) {
|
||||||
|
this.typing = users
|
||||||
|
this.typingSub.notify()
|
||||||
|
}
|
||||||
|
|
||||||
doGarbageCollection() {
|
doGarbageCollection() {
|
||||||
const memberEventsToKeep = new Set<UserID>()
|
const memberEventsToKeep = new Set<UserID>()
|
||||||
const eventsToKeep = new Set<EventRowID>()
|
const eventsToKeep = new Set<EventRowID>()
|
||||||
|
@ -466,6 +549,8 @@ export class RoomStateStore {
|
||||||
const deletedEvents = this.eventsByRowID.size - eventsToKeep.size
|
const deletedEvents = this.eventsByRowID.size - eventsToKeep.size
|
||||||
this.eventsByRowID.clear()
|
this.eventsByRowID.clear()
|
||||||
this.eventsByID.clear()
|
this.eventsByID.clear()
|
||||||
|
this.receiptsByEventID.clear()
|
||||||
|
this.receiptsByUserID.clear()
|
||||||
for (const evt of eventsToKeepList) {
|
for (const evt of eventsToKeepList) {
|
||||||
this.eventsByRowID.set(evt.rowid, evt)
|
this.eventsByRowID.set(evt.rowid, evt)
|
||||||
this.eventsByID.set(evt.event_id, evt)
|
this.eventsByID.set(evt.event_id, evt)
|
||||||
|
|
199
web/src/api/statestore/space.ts
Normal file
|
@ -0,0 +1,199 @@
|
||||||
|
// gomuks - A Matrix client written in Go.
|
||||||
|
// Copyright (C) 2024 Tulir Asokan
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
import { RoomListEntry, StateStore } from "@/api/statestore/main.ts"
|
||||||
|
import { DBSpaceEdge, RoomID } from "@/api/types"
|
||||||
|
import { NonNullCachedEventDispatcher } from "@/util/eventdispatcher.ts"
|
||||||
|
|
||||||
|
export interface RoomListFilter {
|
||||||
|
id: string
|
||||||
|
include(room: RoomListEntry): boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SpaceUnreadCounts {
|
||||||
|
unread_messages: number
|
||||||
|
unread_notifications: number
|
||||||
|
unread_highlights: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const emptyUnreadCounts: SpaceUnreadCounts = {
|
||||||
|
unread_messages: 0,
|
||||||
|
unread_notifications: 0,
|
||||||
|
unread_highlights: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class Space implements RoomListFilter {
|
||||||
|
counts = new NonNullCachedEventDispatcher(emptyUnreadCounts)
|
||||||
|
|
||||||
|
abstract id: string
|
||||||
|
abstract include(room: RoomListEntry): boolean
|
||||||
|
|
||||||
|
clearUnreads() {
|
||||||
|
this.counts.emit(emptyUnreadCounts)
|
||||||
|
}
|
||||||
|
|
||||||
|
applyUnreads(newCounts?: SpaceUnreadCounts | null, oldCounts?: SpaceUnreadCounts | null) {
|
||||||
|
const mergedCounts: SpaceUnreadCounts = {
|
||||||
|
unread_messages: this.counts.current.unread_messages
|
||||||
|
+ (newCounts?.unread_messages ?? 0) - (oldCounts?.unread_messages ?? 0),
|
||||||
|
unread_notifications: this.counts.current.unread_notifications
|
||||||
|
+ (newCounts?.unread_notifications ?? 0) - (oldCounts?.unread_notifications ?? 0),
|
||||||
|
unread_highlights: this.counts.current.unread_highlights
|
||||||
|
+ (newCounts?.unread_highlights ?? 0) - (oldCounts?.unread_highlights ?? 0),
|
||||||
|
}
|
||||||
|
if (mergedCounts.unread_messages < 0) {
|
||||||
|
mergedCounts.unread_messages = 0
|
||||||
|
}
|
||||||
|
if (mergedCounts.unread_notifications < 0) {
|
||||||
|
mergedCounts.unread_notifications = 0
|
||||||
|
}
|
||||||
|
if (mergedCounts.unread_highlights < 0) {
|
||||||
|
mergedCounts.unread_highlights = 0
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
mergedCounts.unread_messages !== this.counts.current.unread_messages
|
||||||
|
|| mergedCounts.unread_notifications !== this.counts.current.unread_notifications
|
||||||
|
|| mergedCounts.unread_highlights !== this.counts.current.unread_highlights
|
||||||
|
) {
|
||||||
|
this.counts.emit(mergedCounts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DirectChatSpace extends Space {
|
||||||
|
id = "fi.mau.gomuks.direct_chats"
|
||||||
|
|
||||||
|
include(room: RoomListEntry): boolean {
|
||||||
|
return Boolean(room.dm_user_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnreadsSpace extends Space {
|
||||||
|
id = "fi.mau.gomuks.unreads"
|
||||||
|
|
||||||
|
constructor(private parent: StateStore) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
include(room: RoomListEntry): boolean {
|
||||||
|
return Boolean(room.room_id === this.parent.activeRoomID
|
||||||
|
|| room.unread_messages
|
||||||
|
|| room.unread_notifications
|
||||||
|
|| room.unread_highlights
|
||||||
|
|| room.marked_unread)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SpaceEdgeStore extends Space {
|
||||||
|
#children: DBSpaceEdge[] = []
|
||||||
|
#childRooms: Set<RoomID> = new Set()
|
||||||
|
#flattenedRooms: Set<RoomID> = new Set()
|
||||||
|
#childSpaces: Set<SpaceEdgeStore> = new Set()
|
||||||
|
readonly #parentSpaces: Set<SpaceEdgeStore> = new Set()
|
||||||
|
|
||||||
|
constructor(public id: RoomID, private parent: StateStore) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
#addParent(parent: SpaceEdgeStore) {
|
||||||
|
this.#parentSpaces.add(parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
#removeParent(parent: SpaceEdgeStore) {
|
||||||
|
this.#parentSpaces.delete(parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
include(room: RoomListEntry) {
|
||||||
|
return this.#flattenedRooms.has(room.room_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
get children() {
|
||||||
|
return this.#children
|
||||||
|
}
|
||||||
|
|
||||||
|
#updateFlattened(recalculate: boolean, added: Set<RoomID>) {
|
||||||
|
if (recalculate) {
|
||||||
|
let flattened = new Set(this.#childRooms)
|
||||||
|
for (const space of this.#childSpaces) {
|
||||||
|
flattened = flattened.union(space.#flattenedRooms)
|
||||||
|
}
|
||||||
|
this.#flattenedRooms = flattened
|
||||||
|
} else if (added.size > 50) {
|
||||||
|
this.#flattenedRooms = this.#flattenedRooms.union(added)
|
||||||
|
} else if (added.size > 0) {
|
||||||
|
for (const room of added) {
|
||||||
|
this.#flattenedRooms.add(room)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#notifyParentsOfChange(recalculate: boolean, added: Set<RoomID>, stack: WeakSet<SpaceEdgeStore>) {
|
||||||
|
if (stack.has(this)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stack.add(this)
|
||||||
|
for (const parent of this.#parentSpaces) {
|
||||||
|
parent.#updateFlattened(recalculate, added)
|
||||||
|
parent.#notifyParentsOfChange(recalculate, added, stack)
|
||||||
|
}
|
||||||
|
stack.delete(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
set children(newChildren: DBSpaceEdge[]) {
|
||||||
|
const newChildRooms = new Set<RoomID>()
|
||||||
|
const newChildSpaces = new Set<SpaceEdgeStore>()
|
||||||
|
for (const child of newChildren) {
|
||||||
|
const spaceStore = this.parent.getSpaceStore(child.child_id)
|
||||||
|
if (spaceStore) {
|
||||||
|
newChildSpaces.add(spaceStore)
|
||||||
|
spaceStore.#addParent(this)
|
||||||
|
} else {
|
||||||
|
newChildRooms.add(child.child_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const space of this.#childSpaces) {
|
||||||
|
if (!newChildSpaces.has(space)) {
|
||||||
|
space.#removeParent(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const addedRooms = newChildRooms.difference(this.#childRooms)
|
||||||
|
const removedRooms = this.#childRooms.difference(newChildRooms)
|
||||||
|
const didAddChildren = newChildSpaces.difference(this.#childSpaces).size > 0
|
||||||
|
const recalculateFlattened = removedRooms.size > 0 || didAddChildren
|
||||||
|
this.#children = newChildren
|
||||||
|
this.#childRooms = newChildRooms
|
||||||
|
this.#childSpaces = newChildSpaces
|
||||||
|
if (this.#childSpaces.size > 0) {
|
||||||
|
this.#updateFlattened(recalculateFlattened, addedRooms)
|
||||||
|
} else {
|
||||||
|
this.#flattenedRooms = newChildRooms
|
||||||
|
}
|
||||||
|
if (this.#parentSpaces.size > 0) {
|
||||||
|
this.#notifyParentsOfChange(recalculateFlattened, addedRooms, new WeakSet())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SpaceOrphansSpace extends SpaceEdgeStore {
|
||||||
|
static id = "fi.mau.gomuks.space_orphans"
|
||||||
|
|
||||||
|
constructor(parent: StateStore) {
|
||||||
|
super(SpaceOrphansSpace.id, parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
include(room: RoomListEntry): boolean {
|
||||||
|
return !super.include(room) && !room.dm_user_id
|
||||||
|
}
|
||||||
|
}
|
30
web/src/api/types/android.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
// gomuks - A Matrix client written in Go.
|
||||||
|
// Copyright (C) 2024 Tulir Asokan
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
export interface AndroidRegisterPushEvent {
|
||||||
|
type: "register_push"
|
||||||
|
device_id: string
|
||||||
|
token: string
|
||||||
|
encryption: { key: string }
|
||||||
|
expiration?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AndroidAuthEvent {
|
||||||
|
type: "auth"
|
||||||
|
authorization: `Bearer ${string}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GomuksAndroidMessageToWeb = AndroidRegisterPushEvent | AndroidAuthEvent
|
|
@ -15,14 +15,18 @@
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import {
|
import {
|
||||||
DBAccountData,
|
DBAccountData,
|
||||||
|
DBInvitedRoom,
|
||||||
|
DBReceipt,
|
||||||
DBRoom,
|
DBRoom,
|
||||||
DBRoomAccountData,
|
DBRoomAccountData,
|
||||||
|
DBSpaceEdge,
|
||||||
EventRowID,
|
EventRowID,
|
||||||
RawDBEvent,
|
RawDBEvent,
|
||||||
TimelineRowTuple,
|
TimelineRowTuple,
|
||||||
} from "./hitypes.ts"
|
} from "./hitypes.ts"
|
||||||
import {
|
import {
|
||||||
DeviceID,
|
DeviceID,
|
||||||
|
EventID,
|
||||||
EventType,
|
EventType,
|
||||||
RoomID,
|
RoomID,
|
||||||
UserID,
|
UserID,
|
||||||
|
@ -68,12 +72,13 @@ export interface ImageAuthTokenEvent extends BaseRPCCommand<string> {
|
||||||
|
|
||||||
export interface SyncRoom {
|
export interface SyncRoom {
|
||||||
meta: DBRoom
|
meta: DBRoom
|
||||||
timeline: TimelineRowTuple[]
|
timeline: TimelineRowTuple[] | null
|
||||||
events: RawDBEvent[]
|
events: RawDBEvent[] | null
|
||||||
state: Record<EventType, Record<string, EventRowID>>
|
state: Record<EventType, Record<string, EventRowID>> | null
|
||||||
reset: boolean
|
reset: boolean
|
||||||
notifications: SyncNotification[]
|
notifications: SyncNotification[] | null
|
||||||
account_data: Record<EventType, DBRoomAccountData>
|
account_data: Record<EventType, DBRoomAccountData> | null
|
||||||
|
receipts: Record<EventID, DBReceipt[]> | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SyncNotification {
|
export interface SyncNotification {
|
||||||
|
@ -82,9 +87,12 @@ export interface SyncNotification {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SyncCompleteData {
|
export interface SyncCompleteData {
|
||||||
rooms: Record<RoomID, SyncRoom>
|
rooms: Record<RoomID, SyncRoom> | null
|
||||||
left_rooms: RoomID[]
|
invited_rooms: DBInvitedRoom[] | null
|
||||||
account_data: Record<EventType, DBAccountData>
|
left_rooms: RoomID[] | null
|
||||||
|
account_data: Record<EventType, DBAccountData> | null
|
||||||
|
space_edges: Record<RoomID, DBSpaceEdge[]> | null
|
||||||
|
top_level_spaces: RoomID[] | null
|
||||||
since?: string
|
since?: string
|
||||||
clear_state?: boolean
|
clear_state?: boolean
|
||||||
}
|
}
|
||||||
|
@ -110,7 +118,7 @@ export interface ClientStateEvent extends BaseRPCCommand<ClientState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SyncStatus {
|
export interface SyncStatus {
|
||||||
type: "ok" | "waiting" | "errored"
|
type: "ok" | "waiting" | "erroring" | "permanently-failed"
|
||||||
error?: string
|
error?: string
|
||||||
error_count: number
|
error_count: number
|
||||||
last_sync?: number
|
last_sync?: number
|
||||||
|
|
|
@ -22,6 +22,7 @@ import {
|
||||||
EventID,
|
EventID,
|
||||||
EventType,
|
EventType,
|
||||||
LazyLoadSummary,
|
LazyLoadSummary,
|
||||||
|
ReceiptType,
|
||||||
RelationType,
|
RelationType,
|
||||||
RoomAlias,
|
RoomAlias,
|
||||||
RoomID,
|
RoomID,
|
||||||
|
@ -53,6 +54,7 @@ export interface DBRoom {
|
||||||
name_quality: RoomNameQuality
|
name_quality: RoomNameQuality
|
||||||
avatar?: ContentURI
|
avatar?: ContentURI
|
||||||
explicit_avatar: boolean
|
explicit_avatar: boolean
|
||||||
|
dm_user_id?: UserID
|
||||||
topic?: string
|
topic?: string
|
||||||
canonical_alias?: RoomAlias
|
canonical_alias?: RoomAlias
|
||||||
lazy_load_summary?: LazyLoadSummary
|
lazy_load_summary?: LazyLoadSummary
|
||||||
|
@ -70,9 +72,34 @@ export interface DBRoom {
|
||||||
prev_batch: string
|
prev_batch: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DBSpaceEdge {
|
||||||
|
// space_id: RoomID
|
||||||
|
child_id: RoomID
|
||||||
|
|
||||||
|
child_event_rowid?: EventRowID
|
||||||
|
order?: string
|
||||||
|
suggested?: true
|
||||||
|
|
||||||
|
parent_event_rowid?: EventRowID
|
||||||
|
canonical?: true
|
||||||
|
}
|
||||||
|
|
||||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export type UnknownEventContent = Record<string, any>
|
export type UnknownEventContent = Record<string, any>
|
||||||
|
|
||||||
|
export interface StrippedStateEvent {
|
||||||
|
type: EventType
|
||||||
|
sender: UserID
|
||||||
|
state_key: string
|
||||||
|
content: UnknownEventContent
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DBInvitedRoom {
|
||||||
|
room_id: RoomID
|
||||||
|
created_at: number
|
||||||
|
invite_state: StrippedStateEvent[]
|
||||||
|
}
|
||||||
|
|
||||||
export enum UnreadType {
|
export enum UnreadType {
|
||||||
None = 0b0000,
|
None = 0b0000,
|
||||||
Normal = 0b0001,
|
Normal = 0b0001,
|
||||||
|
@ -145,8 +172,23 @@ export interface DBRoomAccountData {
|
||||||
content: UnknownEventContent
|
content: UnknownEventContent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DBReceipt {
|
||||||
|
user_id: UserID
|
||||||
|
receipt_type: ReceiptType
|
||||||
|
thread_id?: EventID | "main"
|
||||||
|
event_id: EventID
|
||||||
|
timestamp: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MemReceipt extends DBReceipt {
|
||||||
|
event_rowid: EventRowID
|
||||||
|
timeline_rowid: TimelineRowID
|
||||||
|
}
|
||||||
|
|
||||||
export interface PaginationResponse {
|
export interface PaginationResponse {
|
||||||
events: RawDBEvent[]
|
events: RawDBEvent[]
|
||||||
|
receipts: Record<EventID, DBReceipt[]>
|
||||||
|
related_events: RawDBEvent[]
|
||||||
has_more: boolean
|
has_more: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -242,3 +284,11 @@ export interface ProfileEncryptionInfo {
|
||||||
user_trusted: boolean
|
user_trusted: boolean
|
||||||
errors: string[]
|
errors: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DBPushRegistration {
|
||||||
|
device_id: string
|
||||||
|
type: "fcm"
|
||||||
|
data: unknown
|
||||||
|
encryption: { key: string }
|
||||||
|
expiration?: number
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
export * from "./mxtypes.ts"
|
export * from "./mxtypes.ts"
|
||||||
export * from "./hitypes.ts"
|
export * from "./hitypes.ts"
|
||||||
export * from "./hievents.ts"
|
export * from "./hievents.ts"
|
||||||
|
export * from "./android.ts"
|
||||||
|
|
|
@ -25,6 +25,14 @@ export type RoomVersion = "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" |
|
||||||
export type RoomType = "" | "m.space"
|
export type RoomType = "" | "m.space"
|
||||||
export type RelationType = "m.annotation" | "m.reference" | "m.replace" | "m.thread"
|
export type RelationType = "m.annotation" | "m.reference" | "m.replace" | "m.thread"
|
||||||
|
|
||||||
|
export type JSONValue =
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| null
|
||||||
|
| JSONValue[]
|
||||||
|
| {[key: string]: JSONValue}
|
||||||
|
|
||||||
export interface RoomPredecessor {
|
export interface RoomPredecessor {
|
||||||
room_id: RoomID
|
room_id: RoomID
|
||||||
event_id: EventID
|
event_id: EventID
|
||||||
|
@ -43,7 +51,7 @@ export interface TombstoneEventContent {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LazyLoadSummary {
|
export interface LazyLoadSummary {
|
||||||
heroes?: UserID[]
|
"m.heroes"?: UserID[]
|
||||||
"m.joined_member_count"?: number
|
"m.joined_member_count"?: number
|
||||||
"m.invited_member_count"?: number
|
"m.invited_member_count"?: number
|
||||||
}
|
}
|
||||||
|
@ -65,11 +73,24 @@ export interface EncryptedEventContent {
|
||||||
export interface UserProfile {
|
export interface UserProfile {
|
||||||
displayname?: string
|
displayname?: string
|
||||||
avatar_url?: ContentURI
|
avatar_url?: ContentURI
|
||||||
|
avatar_file?: EncryptedFile
|
||||||
[custom: string]: unknown
|
[custom: string]: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PronounSet {
|
||||||
|
subject?: string
|
||||||
|
object?: string
|
||||||
|
possessive_determiner?: string
|
||||||
|
possessive_pronoun?: string
|
||||||
|
reflexive?: string
|
||||||
|
summary: string
|
||||||
|
language: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Membership = "join" | "leave" | "ban" | "invite" | "knock"
|
||||||
|
|
||||||
export interface MemberEventContent extends UserProfile {
|
export interface MemberEventContent extends UserProfile {
|
||||||
membership: "join" | "leave" | "ban" | "invite" | "knock"
|
membership: Membership
|
||||||
reason?: string
|
reason?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,6 +112,12 @@ export interface ACLEventContent {
|
||||||
deny?: string[]
|
deny?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PolicyRuleContent {
|
||||||
|
entity: string
|
||||||
|
reason: string
|
||||||
|
recommendation: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface PowerLevelEventContent {
|
export interface PowerLevelEventContent {
|
||||||
users?: Record<UserID, number>
|
users?: Record<UserID, number>
|
||||||
users_default?: number
|
users_default?: number
|
||||||
|
@ -138,6 +165,23 @@ export interface ContentWarning {
|
||||||
description?: string
|
description?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface URLPreview {
|
||||||
|
matched_url: string
|
||||||
|
"beeper:image:encryption"?: EncryptedFile
|
||||||
|
"matrix:image:size": number
|
||||||
|
"og:image"?: ContentURI
|
||||||
|
"og:url": string
|
||||||
|
"og:image:width"?: number
|
||||||
|
"og:image:height"?: number
|
||||||
|
"og:image:type"?: string
|
||||||
|
"og:title"?: string
|
||||||
|
"og:description"?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BeeperPerMessageProfile extends UserProfile {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface BaseMessageEventContent {
|
export interface BaseMessageEventContent {
|
||||||
msgtype: string
|
msgtype: string
|
||||||
body: string
|
body: string
|
||||||
|
@ -148,6 +192,9 @@ export interface BaseMessageEventContent {
|
||||||
"town.robin.msc3725.content_warning"?: ContentWarning
|
"town.robin.msc3725.content_warning"?: ContentWarning
|
||||||
"page.codeberg.everypizza.msc4193.spoiler"?: boolean
|
"page.codeberg.everypizza.msc4193.spoiler"?: boolean
|
||||||
"page.codeberg.everypizza.msc4193.spoiler.reason"?: string
|
"page.codeberg.everypizza.msc4193.spoiler.reason"?: string
|
||||||
|
"m.url_previews"?: URLPreview[]
|
||||||
|
"com.beeper.linkpreviews"?: URLPreview[]
|
||||||
|
"com.beeper.per_message_profile"?: BeeperPerMessageProfile
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TextMessageEventContent extends BaseMessageEventContent {
|
export interface TextMessageEventContent extends BaseMessageEventContent {
|
||||||
|
@ -155,7 +202,7 @@ export interface TextMessageEventContent extends BaseMessageEventContent {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MediaMessageEventContent extends BaseMessageEventContent {
|
export interface MediaMessageEventContent extends BaseMessageEventContent {
|
||||||
msgtype: "m.image" | "m.file" | "m.audio" | "m.video"
|
msgtype: "m.sticker" | "m.image" | "m.file" | "m.audio" | "m.video"
|
||||||
filename?: string
|
filename?: string
|
||||||
url?: ContentURI
|
url?: ContentURI
|
||||||
file?: EncryptedFile
|
file?: EncryptedFile
|
||||||
|
@ -235,3 +282,37 @@ export interface ImagePackRooms {
|
||||||
export interface ElementRecentEmoji {
|
export interface ElementRecentEmoji {
|
||||||
recent_emoji: [string, number][]
|
recent_emoji: [string, number][]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type JoinRule = "public" | "knock" | "restricted" | "knock_restricted" | "invite" | "private"
|
||||||
|
|
||||||
|
export interface RoomSummary {
|
||||||
|
room_id: RoomID
|
||||||
|
membership?: Membership
|
||||||
|
|
||||||
|
room_version?: RoomVersion
|
||||||
|
"im.nheko.summary.room_version"?: RoomVersion
|
||||||
|
"im.nheko.summary.version"?: RoomVersion
|
||||||
|
encryption?: "m.megolm.v1.aes-sha2"
|
||||||
|
"im.nheko.summary.encryption"?: "m.megolm.v1.aes-sha2"
|
||||||
|
|
||||||
|
avatar_url?: ContentURI
|
||||||
|
canonical_alias?: RoomAlias
|
||||||
|
guest_can_join: boolean
|
||||||
|
join_rule?: JoinRule
|
||||||
|
name?: string
|
||||||
|
num_joined_members: number
|
||||||
|
room_type: RoomType
|
||||||
|
topic?: string
|
||||||
|
world_readable: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RespRoomJoin {
|
||||||
|
room_id: RoomID
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RespOpenIDToken {
|
||||||
|
access_token: string
|
||||||
|
expires_in: number
|
||||||
|
matrix_server_name: string
|
||||||
|
token_type: "Bearer"
|
||||||
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import type { ContentURI } from "../../types"
|
import type { ContentURI } from "../../types"
|
||||||
import { Preference, anyContext } from "./types.ts"
|
import { Preference, anyContext, anyGlobalContext } from "./types.ts"
|
||||||
|
|
||||||
export const codeBlockStyles = [
|
export const codeBlockStyles = [
|
||||||
"auto", "abap", "algol_nu", "algol", "arduino", "autumn", "average", "base16-snazzy", "borland", "bw",
|
"auto", "abap", "algol_nu", "algol", "arduino", "autumn", "average", "base16-snazzy", "borland", "bw",
|
||||||
|
@ -47,6 +47,12 @@ export const preferences = {
|
||||||
allowedContexts: anyContext,
|
allowedContexts: anyContext,
|
||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
}),
|
}),
|
||||||
|
display_read_receipts: new Preference<boolean>({
|
||||||
|
displayName: "Display read receipts",
|
||||||
|
description: "Should read receipts be rendered in the timeline?",
|
||||||
|
allowedContexts: anyContext,
|
||||||
|
defaultValue: true,
|
||||||
|
}),
|
||||||
show_media_previews: new Preference<boolean>({
|
show_media_previews: new Preference<boolean>({
|
||||||
displayName: "Show image and video previews",
|
displayName: "Show image and video previews",
|
||||||
description: "If disabled, images and videos will only be visible after clicking and will not be downloaded automatically.",
|
description: "If disabled, images and videos will only be visible after clicking and will not be downloaded automatically.",
|
||||||
|
@ -96,6 +102,18 @@ export const preferences = {
|
||||||
allowedContexts: anyContext,
|
allowedContexts: anyContext,
|
||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
}),
|
}),
|
||||||
|
render_url_previews: new Preference<boolean>({
|
||||||
|
displayName: "Render URL previews",
|
||||||
|
description: "Whether to render MSC4095 URL previews in the room timeline.",
|
||||||
|
allowedContexts: anyContext,
|
||||||
|
defaultValue: true,
|
||||||
|
}),
|
||||||
|
small_replies: new Preference<boolean>({
|
||||||
|
displayName: "Compact reply style",
|
||||||
|
description: "Whether to use a Discord-like compact style for replies instead of the traditional style.",
|
||||||
|
allowedContexts: anyContext,
|
||||||
|
defaultValue: false,
|
||||||
|
}),
|
||||||
show_date_separators: new Preference<boolean>({
|
show_date_separators: new Preference<boolean>({
|
||||||
displayName: "Show date separators",
|
displayName: "Show date separators",
|
||||||
description: "Whether messages in different days should have a date separator between them in the room timeline.",
|
description: "Whether messages in different days should have a date separator between them in the room timeline.",
|
||||||
|
@ -135,12 +153,42 @@ export const preferences = {
|
||||||
// allowedContexts: anyContext,
|
// allowedContexts: anyContext,
|
||||||
// defaultValue: false,
|
// defaultValue: false,
|
||||||
// }),
|
// }),
|
||||||
|
message_context_menu: new Preference<boolean>({
|
||||||
|
displayName: "Right-click menu on messages",
|
||||||
|
description: "Show a context menu when right-clicking on messages.",
|
||||||
|
allowedContexts: anyContext,
|
||||||
|
defaultValue: true,
|
||||||
|
}),
|
||||||
|
ctrl_enter_send: new Preference<boolean>({
|
||||||
|
displayName: "Use Ctrl+Enter to send",
|
||||||
|
description: "Disable sending on enter and use Ctrl+Enter for sending instead",
|
||||||
|
allowedContexts: anyContext,
|
||||||
|
defaultValue: false,
|
||||||
|
}),
|
||||||
custom_notification_sound: new Preference<ContentURI>({
|
custom_notification_sound: new Preference<ContentURI>({
|
||||||
displayName: "Custom notification sound",
|
displayName: "Custom notification sound",
|
||||||
description: "The mxc:// URI to a custom notification sound.",
|
description: "The mxc:// URI to a custom notification sound.",
|
||||||
allowedContexts: anyContext,
|
allowedContexts: anyContext,
|
||||||
defaultValue: "",
|
defaultValue: "",
|
||||||
}),
|
}),
|
||||||
|
room_window_title: new Preference<string>({
|
||||||
|
displayName: "In-room window title",
|
||||||
|
description: "The title to use for the window when viewing a room. $room will be replaced with the room name",
|
||||||
|
allowedContexts: anyContext,
|
||||||
|
defaultValue: "$room - gomuks web",
|
||||||
|
}),
|
||||||
|
window_title: new Preference<string>({
|
||||||
|
displayName: "Default window title",
|
||||||
|
description: "The title to use for the window when not in a room.",
|
||||||
|
allowedContexts: anyGlobalContext,
|
||||||
|
defaultValue: "gomuks web",
|
||||||
|
}),
|
||||||
|
favicon: new Preference<string>({
|
||||||
|
displayName: "Favicon",
|
||||||
|
description: "The URL to use for the favicon.",
|
||||||
|
allowedContexts: anyContext,
|
||||||
|
defaultValue: "gomuks.png",
|
||||||
|
}),
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export const existingPreferenceKeys = new Set(Object.keys(preferences))
|
export const existingPreferenceKeys = new Set(Object.keys(preferences))
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { PreferenceContext, PreferenceValueType } from "./types.ts"
|
||||||
|
|
||||||
const prefKeys = Object.keys(preferences)
|
const prefKeys = Object.keys(preferences)
|
||||||
|
|
||||||
export function getPreferenceProxy(store: StateStore, room?: RoomStateStore): Preferences {
|
export function getPreferenceProxy(store: StateStore, room?: RoomStateStore): Required<Preferences> {
|
||||||
return new Proxy({}, {
|
return new Proxy({}, {
|
||||||
set(): boolean {
|
set(): boolean {
|
||||||
throw new Error("The preference proxy is read-only")
|
throw new Error("The preference proxy is read-only")
|
||||||
|
@ -61,5 +61,5 @@ export function getPreferenceProxy(store: StateStore, room?: RoomStateStore): Pr
|
||||||
writable: false,
|
writable: false,
|
||||||
} : undefined
|
} : undefined
|
||||||
},
|
},
|
||||||
})
|
}) as Required<Preferences>
|
||||||
}
|
}
|
||||||
|
|
1
web/src/icons/devices-off.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="m354-720-80-80h566v80H354Zm526 520-80-80v-280H640v126l-80-80v-86q0-17 11.5-28.5T600-640h240q17 0 28.5 11.5T880-600v400ZM792-56 688-160h-88q-17 0-28.5-11.5T560-200v-88L240-608v328h240v120H80v-120h80v-408L56-792l56-56 736 736-56 56Zm-72-301Z"/></svg>
|
After Width: | Height: | Size: 365 B |
1
web/src/icons/devices.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M80-160v-120h80v-440q0-33 23.5-56.5T240-800h600v80H240v440h240v120H80Zm520 0q-17 0-28.5-11.5T560-200v-400q0-17 11.5-28.5T600-640h240q17 0 28.5 11.5T880-600v400q0 17-11.5 28.5T840-160H600Zm40-120h160v-280H640v280Zm0 0h160-160Z"/></svg>
|
After Width: | Height: | Size: 351 B |
1
web/src/icons/home.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M240-200h120v-240h240v240h120v-360L480-740 240-560v360Zm-80 80v-480l320-240 320 240v480H520v-240h-80v240H160Zm320-350Z"/></svg>
|
After Width: | Height: | Size: 244 B |
1
web/src/icons/notifications-off.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M160-200v-80h80v-280q0-33 8.5-65t25.5-61l60 60q-7 16-10.5 32.5T320-560v280h248L56-792l56-56 736 736-56 56-146-144H160Zm560-154-80-80v-126q0-66-47-113t-113-47q-26 0-50 8t-44 24l-58-58q20-16 43-28t49-18v-28q0-25 17.5-42.5T480-880q25 0 42.5 17.5T540-820v28q80 20 130 84.5T720-560v206Zm-276-50Zm36 324q-33 0-56.5-23.5T400-160h160q0 33-23.5 56.5T480-80Zm33-481Z"/></svg>
|
After Width: | Height: | Size: 482 B |
1
web/src/icons/notifications-unread.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M480-80q-33 0-56.5-23.5T400-160h160q0 33-23.5 56.5T480-80Zm0-420ZM160-200v-80h80v-280q0-83 50-147.5T420-792v-28q0-25 17.5-42.5T480-880q25 0 42.5 17.5T540-820v13q-11 22-16 45t-4 47q-10-2-19.5-3.5T480-720q-66 0-113 47t-47 113v280h320v-257q18 8 38.5 12.5T720-520v240h80v80H160Zm560-400q-50 0-85-35t-35-85q0-50 35-85t85-35q50 0 85 35t35 85q0 50-35 85t-85 35Z"/></svg>
|
After Width: | Height: | Size: 480 B |
1
web/src/icons/notifications.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M160-200v-80h80v-280q0-83 50-147.5T420-792v-28q0-25 17.5-42.5T480-880q25 0 42.5 17.5T540-820v28q80 20 130 84.5T720-560v280h80v80H160Zm320-300Zm0 420q-33 0-56.5-23.5T400-160h160q0 33-23.5 56.5T480-80ZM320-280h320v-280q0-66-47-113t-113-47q-66 0-113 47t-47 113v280Z"/></svg>
|
After Width: | Height: | Size: 388 B |
1
web/src/icons/person.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M480-480q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 66-47 113t-113 47ZM160-160v-112q0-34 17.5-62.5T224-378q62-31 126-46.5T480-440q66 0 130 15.5T736-378q29 15 46.5 43.5T800-272v112H160Zm80-80h480v-32q0-11-5.5-20T700-306q-54-27-109-40.5T480-360q-56 0-111 13.5T260-306q-9 5-14.5 14t-5.5 20v32Zm240-320q33 0 56.5-23.5T560-640q0-33-23.5-56.5T480-720q-33 0-56.5 23.5T400-640q0 33 23.5 56.5T480-560Zm0-80Zm0 400Z"/></svg>
|
After Width: | Height: | Size: 549 B |
1
web/src/icons/tag.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="m240-160 40-160H120l20-80h160l40-160H180l20-80h160l40-160h80l-40 160h160l40-160h80l-40 160h160l-20 80H660l-40 160h160l-20 80H600l-40 160h-80l40-160H360l-40 160h-80Zm140-240h160l40-160H420l-40 160Z"/></svg>
|
After Width: | Height: | Size: 322 B |
1
web/src/icons/thread.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M554-120q-54 0-91-37t-37-89q0-76 61.5-137.5T641-460q-3-36-18-54.5T582-533q-30 0-65 25t-83 82q-78 93-114.5 121T241-277q-51 0-86-38t-35-92q0-54 23.5-110.5T223-653q19-26 28-44t9-29q0-7-2.5-10.5T250-740q-10 0-25 12.5T190-689l-70-71q32-39 65-59.5t65-20.5q46 0 78 32t32 80q0 29-15 64t-50 84q-38 54-56.5 95T220-413q0 17 5.5 26.5T241-377q10 0 17.5-5.5T286-409q13-14 31-34.5t44-50.5q63-75 114-107t107-32q67 0 110 45t49 123h99v100h-99q-8 112-58.5 178.5T554-120Zm2-100q32 0 54-36.5T640-358q-46 11-80 43.5T526-250q0 14 8 22t22 8Z"/></svg>
|
After Width: | Height: | Size: 643 B |
|
@ -11,6 +11,7 @@
|
||||||
--semisecondary-text-color: #555;
|
--semisecondary-text-color: #555;
|
||||||
--link-text-color: #0467dd;
|
--link-text-color: #0467dd;
|
||||||
--visited-link-text-color: var(--link-text-color);
|
--visited-link-text-color: var(--link-text-color);
|
||||||
|
--small-font-size: .875rem;
|
||||||
|
|
||||||
--code-background-color: rgba(0, 0, 0, 0.15);
|
--code-background-color: rgba(0, 0, 0, 0.15);
|
||||||
--media-placeholder-default-background: rgba(0, 0, 0, .1);
|
--media-placeholder-default-background: rgba(0, 0, 0, .1);
|
||||||
|
@ -22,11 +23,14 @@
|
||||||
|
|
||||||
--border-color: #ccc;
|
--border-color: #ccc;
|
||||||
--pill-background-color: #ccc;
|
--pill-background-color: #ccc;
|
||||||
|
--url-preview-background-color: rgba(0, 0, 0, .05);
|
||||||
--highlight-pill-background-color: #c00;
|
--highlight-pill-background-color: #c00;
|
||||||
--highlight-pill-text-color: #fff;
|
--highlight-pill-text-color: #fff;
|
||||||
--button-hover-color: rgba(0, 0, 0, .2);
|
--button-hover-color: rgba(0, 0, 0, .2);
|
||||||
--light-hover-color: rgba(0, 0, 0, .1);
|
--light-hover-color: rgba(0, 0, 0, .1);
|
||||||
|
|
||||||
|
--composer-background-color: #f0f0f0;
|
||||||
|
|
||||||
--timeline-hover-bg-color: #eee;
|
--timeline-hover-bg-color: #eee;
|
||||||
--timeline-highlight-bg-color: rgba(255, 255, 0, .1);
|
--timeline-highlight-bg-color: rgba(255, 255, 0, .1);
|
||||||
--timeline-highlight-hover-bg-color: #eec;
|
--timeline-highlight-hover-bg-color: #eec;
|
||||||
|
@ -42,7 +46,7 @@
|
||||||
--room-list-entry-selected-color: rgba(0, 0, 0, 0.125);
|
--room-list-entry-selected-color: rgba(0, 0, 0, 0.125);
|
||||||
|
|
||||||
--dimmed-overlay-background-color: rgba(0, 0, 0, .75);
|
--dimmed-overlay-background-color: rgba(0, 0, 0, .75);
|
||||||
--modal-box-shadow-color: rgba(0, 0, 0, 0.15);
|
--modal-box-shadow-color: rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
--emoji-selected-border-color: #cec;
|
--emoji-selected-border-color: #cec;
|
||||||
|
|
||||||
|
@ -51,6 +55,9 @@
|
||||||
--unread-counter-notification-bg: rgba(50, 150, 0, 0.7);
|
--unread-counter-notification-bg: rgba(50, 150, 0, 0.7);
|
||||||
--unread-counter-marked-unread-bg: var(--unread-counter-notification-bg);
|
--unread-counter-marked-unread-bg: var(--unread-counter-notification-bg);
|
||||||
--unread-counter-highlight-bg: rgba(200, 0, 0, 0.7);
|
--unread-counter-highlight-bg: rgba(200, 0, 0, 0.7);
|
||||||
|
--space-unread-counter-message-bg: rgb(100, 100, 100, 0.9);
|
||||||
|
--space-unread-counter-notification-bg: rgb(50, 150, 0);
|
||||||
|
--space-unread-counter-highlight-bg: rgb(200, 0, 0);
|
||||||
|
|
||||||
--sender-color-0: #a4041d;
|
--sender-color-0: #a4041d;
|
||||||
--sender-color-1: #9b2200;
|
--sender-color-1: #9b2200;
|
||||||
|
@ -79,6 +86,13 @@
|
||||||
--timeline-message-gap-small-event: 0;
|
--timeline-message-gap-small-event: 0;
|
||||||
--timeline-sender-name-timestamp-gap: .25rem;
|
--timeline-sender-name-timestamp-gap: .25rem;
|
||||||
--timeline-sender-name-content-gap: 0;
|
--timeline-sender-name-content-gap: 0;
|
||||||
|
--timeline-horizontal-padding: 1.5rem;
|
||||||
|
--timeline-status-size: 4rem;
|
||||||
|
|
||||||
|
@media screen and (max-width: 45rem) {
|
||||||
|
--timeline-horizontal-padding: .5rem;
|
||||||
|
--timeline-status-size: 2.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
|
@ -100,9 +114,12 @@
|
||||||
|
|
||||||
--border-color: #222;
|
--border-color: #222;
|
||||||
--pill-background-color: #222;
|
--pill-background-color: #222;
|
||||||
|
--url-preview-background-color: #222;
|
||||||
--button-hover-color: rgba(255, 255, 255, .2);
|
--button-hover-color: rgba(255, 255, 255, .2);
|
||||||
--light-hover-color: rgba(255, 255, 255, .1);
|
--light-hover-color: rgba(255, 255, 255, .1);
|
||||||
|
|
||||||
|
--composer-background-color: #0a0a0a;
|
||||||
|
|
||||||
--timeline-hover-bg-color: #111;
|
--timeline-hover-bg-color: #111;
|
||||||
--timeline-highlight-bg-color: rgba(255, 255, 0, .1);
|
--timeline-highlight-bg-color: rgba(255, 255, 0, .1);
|
||||||
--timeline-highlight-hover-bg-color: #331;
|
--timeline-highlight-hover-bg-color: #331;
|
||||||
|
@ -115,13 +132,16 @@
|
||||||
--room-list-entry-hover-color: rgba(255, 255, 255, 0.075);
|
--room-list-entry-hover-color: rgba(255, 255, 255, 0.075);
|
||||||
--room-list-entry-selected-color: rgba(255, 255, 255, 0.125);
|
--room-list-entry-selected-color: rgba(255, 255, 255, 0.125);
|
||||||
|
|
||||||
--modal-box-shadow-color: rgba(255, 255, 255, 0.1);
|
--modal-box-shadow-color: rgba(255, 255, 255, 0.04);
|
||||||
|
|
||||||
--emoji-selected-border-color: #131;
|
--emoji-selected-border-color: #131;
|
||||||
|
|
||||||
--unread-counter-message-bg: rgba(255, 255, 255, 0.5);
|
--unread-counter-message-bg: rgba(255, 255, 255, 0.5);
|
||||||
--unread-counter-notification-bg: rgba(150, 255, 0, 0.7);
|
--unread-counter-notification-bg: rgba(150, 255, 0, 0.7);
|
||||||
--unread-counter-highlight-bg: rgba(255, 50, 50, 0.7);
|
--unread-counter-highlight-bg: rgba(255, 50, 50, 0.7);
|
||||||
|
--space-unread-counter-message-bg: rgb(200, 200, 200, 0.8);
|
||||||
|
--space-unread-counter-notification-bg: rgb(150, 255, 0);
|
||||||
|
--space-unread-counter-highlight-bg: rgb(255, 50, 50);
|
||||||
|
|
||||||
--sender-color-0: #ff877c;
|
--sender-color-0: #ff877c;
|
||||||
--sender-color-1: #f6913d;
|
--sender-color-1: #f6913d;
|
||||||
|
@ -144,15 +164,17 @@ body {
|
||||||
font-family: var(--font-stack);
|
font-family: var(--font-stack);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background-color: var(--login-background-color);
|
background-color: var(--background-color);
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
|
background-color: var(--background-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
#root {
|
#root {
|
||||||
|
@ -232,9 +254,15 @@ div.connection-error-wrapper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
div.pre-connect {
|
div.pre-main {
|
||||||
margin-top: 2rem;
|
position: fixed;
|
||||||
text-align: center;
|
inset: 0;
|
||||||
|
background-color: var(--login-background-color);
|
||||||
|
|
||||||
|
&.waiting-to-connect {
|
||||||
|
padding-top: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
main.matrix-main {
|
main.matrix-main {
|
||||||
--room-list-width: 300px;
|
--room-list-width: 350px;
|
||||||
--right-panel-width: 300px;
|
--right-panel-width: 300px;
|
||||||
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
@ -16,35 +16,36 @@ main.matrix-main {
|
||||||
/ var(--room-list-width) 0 1fr 0 var(--right-panel-width);
|
/ var(--room-list-width) 0 1fr 0 var(--right-panel-width);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 750px) {
|
@media screen and (max-width: 45rem) {
|
||||||
|
&, &.right-panel-open {
|
||||||
|
grid-template:
|
||||||
|
"roomlist roomview rightpanel" 1fr
|
||||||
|
/ 100% 100% 100%;
|
||||||
|
}
|
||||||
|
/* Note: this timeout must match the one in MainScreen.tsx */
|
||||||
|
transition: .3s;
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.room-selected {
|
||||||
|
translate: -100% 0;
|
||||||
|
}
|
||||||
|
|
||||||
&.right-panel-open {
|
&.right-panel-open {
|
||||||
grid-template: "rightpanel" 1fr / 1fr;
|
translate: -200% 0;
|
||||||
> div.room-list-wrapper {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
> div.room-view {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.room-selected:not(.right-panel-open) {
|
|
||||||
grid-template: "roomview" 1fr / 1fr;
|
|
||||||
> div.room-list-wrapper {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(.room-selected):not(.right-panel-open) {
|
|
||||||
grid-template: "roomlist" 1fr / 1fr;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> div.room-list-resizer {
|
> div.room-list-resizer {
|
||||||
grid-area: rh1;
|
grid-area: rh1;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
> div.right-panel-resizer {
|
> div.right-panel-resizer {
|
||||||
grid-area: rh2;
|
grid-area: rh2;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,20 +13,21 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import { JSX, use, useEffect, useInsertionEffect, useLayoutEffect, useMemo, useState } from "react"
|
import { JSX, use, useEffect, useMemo, useReducer, useRef, useState } from "react"
|
||||||
import { SyncLoader } from "react-spinners"
|
import { SyncLoader } from "react-spinners"
|
||||||
import Client from "@/api/client.ts"
|
import Client from "@/api/client.ts"
|
||||||
import { RoomStateStore } from "@/api/statestore"
|
import { RoomListFilter, RoomStateStore } from "@/api/statestore"
|
||||||
import type { RoomID } from "@/api/types"
|
import type { RoomID } from "@/api/types"
|
||||||
import { useEventAsState } from "@/util/eventdispatcher.ts"
|
import { useEventAsState } from "@/util/eventdispatcher.ts"
|
||||||
import { parseMatrixURI } from "@/util/validation.ts"
|
import { ensureString, ensureStringArray, parseMatrixURI } from "@/util/validation.ts"
|
||||||
import ClientContext from "./ClientContext.ts"
|
import ClientContext from "./ClientContext.ts"
|
||||||
import MainScreenContext, { MainScreenContextFields } from "./MainScreenContext.ts"
|
import MainScreenContext, { MainScreenContextFields } from "./MainScreenContext.ts"
|
||||||
import StylePreferences from "./StylePreferences.tsx"
|
import StylePreferences from "./StylePreferences.tsx"
|
||||||
import Keybindings from "./keybindings.ts"
|
import Keybindings from "./keybindings.ts"
|
||||||
import { ModalWrapper } from "./modal/Modal.tsx"
|
import { ModalWrapper } from "./modal"
|
||||||
import RightPanel, { RightPanelProps } from "./rightpanel/RightPanel.tsx"
|
import RightPanel, { RightPanelProps } from "./rightpanel/RightPanel.tsx"
|
||||||
import RoomList from "./roomlist/RoomList.tsx"
|
import RoomList from "./roomlist/RoomList.tsx"
|
||||||
|
import RoomPreview, { RoomPreviewProps } from "./roomview/RoomPreview.tsx"
|
||||||
import RoomView from "./roomview/RoomView.tsx"
|
import RoomView from "./roomview/RoomView.tsx"
|
||||||
import { useResizeHandle } from "./util/useResizeHandle.tsx"
|
import { useResizeHandle } from "./util/useResizeHandle.tsx"
|
||||||
import "./MainScreen.css"
|
import "./MainScreen.css"
|
||||||
|
@ -50,7 +51,8 @@ class ContextFields implements MainScreenContextFields {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private directSetRightPanel: (props: RightPanelProps | null) => void,
|
private directSetRightPanel: (props: RightPanelProps | null) => void,
|
||||||
private directSetActiveRoom: (room: RoomStateStore | null) => void,
|
private directSetActiveRoom: (room: RoomStateStore | RoomPreviewProps | null) => void,
|
||||||
|
private directSetSpace: (space: RoomListFilter | null) => void,
|
||||||
private client: Client,
|
private client: Client,
|
||||||
) {
|
) {
|
||||||
this.keybindings = new Keybindings(client.store, this)
|
this.keybindings = new Keybindings(client.store, this)
|
||||||
|
@ -94,33 +96,115 @@ class ContextFields implements MainScreenContextFields {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setActiveRoom = (roomID: RoomID | null, pushState = true) => {
|
setActiveRoom = (
|
||||||
|
roomID: RoomID | null,
|
||||||
|
previewMeta?: Partial<RoomPreviewProps>,
|
||||||
|
toSpace?: RoomListFilter,
|
||||||
|
pushState = true,
|
||||||
|
) => {
|
||||||
console.log("Switching to room", roomID)
|
console.log("Switching to room", roomID)
|
||||||
const room = (roomID && this.client.store.rooms.get(roomID)) || null
|
if (roomID) {
|
||||||
|
const room = this.client.store.rooms.get(roomID)
|
||||||
|
if (room) {
|
||||||
|
this.#setActiveRoom(room, toSpace, pushState)
|
||||||
|
} else {
|
||||||
|
this.#setPreviewRoom(roomID, pushState, previewMeta)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.#closeActiveRoom(pushState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSpace = (space: RoomListFilter | null, pushState = true) => {
|
||||||
|
if (space === this.client.store.currentRoomListFilter) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log("Switching to space", space?.id)
|
||||||
|
this.directSetSpace(space)
|
||||||
|
this.client.store.currentRoomListFilter = space
|
||||||
|
if (pushState) {
|
||||||
|
if (this.client.store.activeRoomID && space) {
|
||||||
|
const entry = this.client.store.roomListEntries.get(this.client.store.activeRoomID)
|
||||||
|
if (entry && !space.include(entry)) {
|
||||||
|
this.setActiveRoom(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
history.replaceState({ ...(history.state || {}), space_id: space?.id }, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#setPreviewRoom(roomID: RoomID, pushState: boolean, meta?: Partial<RoomPreviewProps>) {
|
||||||
|
const invite = this.client.store.inviteRooms.get(roomID)
|
||||||
|
this.#closeActiveRoom(false)
|
||||||
|
this.directSetActiveRoom({ roomID, ...(meta ?? {}), invite })
|
||||||
|
this.client.store.activeRoomID = roomID
|
||||||
|
this.client.store.activeRoomIsPreview = true
|
||||||
|
if (pushState) {
|
||||||
|
history.pushState({
|
||||||
|
room_id: roomID,
|
||||||
|
source_via: meta?.via,
|
||||||
|
source_alias: meta?.alias,
|
||||||
|
space_id: history.state?.space_id,
|
||||||
|
}, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#getWindowTitle(room?: RoomStateStore, name?: string) {
|
||||||
|
if (!room) {
|
||||||
|
return this.client.store.preferences.window_title
|
||||||
|
}
|
||||||
|
return room.preferences.room_window_title.replace("$room", name!)
|
||||||
|
}
|
||||||
|
|
||||||
|
#setActiveRoom(room: RoomStateStore, space: RoomListFilter | undefined | null, pushState: boolean) {
|
||||||
window.activeRoom = room
|
window.activeRoom = room
|
||||||
this.directSetActiveRoom(room)
|
this.directSetActiveRoom(room)
|
||||||
this.directSetRightPanel(null)
|
this.directSetRightPanel(null)
|
||||||
this.rightPanelStack = []
|
if (!space && this.client.store.currentRoomListFilter) {
|
||||||
this.client.store.activeRoomID = room?.roomID ?? null
|
const roomListEntry = this.client.store.roomListEntries.get(room.roomID)
|
||||||
this.keybindings.activeRoom = room
|
if (roomListEntry && !this.client.store.currentRoomListFilter.include(roomListEntry)) {
|
||||||
if (room) {
|
space = this.client.store.findMatchingSpace(roomListEntry)
|
||||||
room.lastOpened = Date.now()
|
|
||||||
if (!room.stateLoaded) {
|
|
||||||
this.client.loadRoomState(room.roomID)
|
|
||||||
.catch(err => console.error("Failed to load room state", err))
|
|
||||||
}
|
}
|
||||||
document
|
|
||||||
.querySelector(`div.room-entry[data-room-id="${CSS.escape(room.roomID)}"]`)
|
|
||||||
?.scrollIntoView({ block: "nearest" })
|
|
||||||
}
|
}
|
||||||
|
if (space && space !== this.client.store.currentRoomListFilter) {
|
||||||
|
console.log("Switching to space", space?.id)
|
||||||
|
this.directSetSpace(space)
|
||||||
|
this.client.store.currentRoomListFilter = space
|
||||||
|
}
|
||||||
|
this.rightPanelStack = []
|
||||||
|
this.client.store.activeRoomID = room.roomID
|
||||||
|
this.client.store.activeRoomIsPreview = false
|
||||||
|
this.keybindings.activeRoom = room
|
||||||
|
room.lastOpened = Date.now()
|
||||||
|
if (!room.stateLoaded) {
|
||||||
|
this.client.loadRoomState(room.roomID)
|
||||||
|
.catch(err => console.error("Failed to load room state", err))
|
||||||
|
}
|
||||||
|
document
|
||||||
|
.querySelector(`div.room-entry[data-room-id="${CSS.escape(room.roomID)}"]`)
|
||||||
|
?.scrollIntoView({ block: "nearest" })
|
||||||
if (pushState) {
|
if (pushState) {
|
||||||
history.pushState({ room_id: roomID }, "")
|
history.pushState({ room_id: room.roomID, space_id: space?.id ?? history.state?.space_id }, "")
|
||||||
}
|
}
|
||||||
let roomNameForTitle = room?.meta.current.name
|
let roomNameForTitle = room.meta.current.name
|
||||||
if (roomNameForTitle && roomNameForTitle.length > 48) {
|
if (roomNameForTitle && roomNameForTitle.length > 48) {
|
||||||
roomNameForTitle = roomNameForTitle.slice(0, 45) + "…"
|
roomNameForTitle = roomNameForTitle.slice(0, 45) + "…"
|
||||||
}
|
}
|
||||||
document.title = roomNameForTitle ? `${roomNameForTitle} - gomuks web` : "gomuks web"
|
document.title = this.#getWindowTitle(room, roomNameForTitle)
|
||||||
|
}
|
||||||
|
|
||||||
|
#closeActiveRoom(pushState: boolean) {
|
||||||
|
window.activeRoom = null
|
||||||
|
this.directSetActiveRoom(null)
|
||||||
|
this.directSetRightPanel(null)
|
||||||
|
this.rightPanelStack = []
|
||||||
|
this.client.store.activeRoomID = null
|
||||||
|
this.client.store.activeRoomIsPreview = false
|
||||||
|
this.keybindings.activeRoom = null
|
||||||
|
if (pushState) {
|
||||||
|
history.pushState({ space_id: history.state?.space_id }, "")
|
||||||
|
}
|
||||||
|
document.title = this.#getWindowTitle()
|
||||||
}
|
}
|
||||||
|
|
||||||
clickRoom = (evt: React.MouseEvent) => {
|
clickRoom = (evt: React.MouseEvent) => {
|
||||||
|
@ -133,6 +217,7 @@ class ContextFields implements MainScreenContextFields {
|
||||||
}
|
}
|
||||||
|
|
||||||
clickRightPanelOpener = (evt: React.MouseEvent) => {
|
clickRightPanelOpener = (evt: React.MouseEvent) => {
|
||||||
|
evt.preventDefault()
|
||||||
const type = evt.currentTarget.getAttribute("data-target-panel")
|
const type = evt.currentTarget.getAttribute("data-target-panel")
|
||||||
if (type === "pinned-messages" || type === "members") {
|
if (type === "pinned-messages" || type === "members") {
|
||||||
this.setRightPanel({ type })
|
this.setRightPanel({ type })
|
||||||
|
@ -149,8 +234,11 @@ class ContextFields implements MainScreenContextFields {
|
||||||
|
|
||||||
const SYNC_ERROR_HIDE_DELAY = 30 * 1000
|
const SYNC_ERROR_HIDE_DELAY = 30 * 1000
|
||||||
|
|
||||||
const handleURLHash = (client: Client) => {
|
const handleURLHash = (client: Client, context: MainScreenContextFields, hashOnly = false) => {
|
||||||
if (!location.hash.startsWith("#/uri/")) {
|
if (!location.hash.startsWith("#/uri/")) {
|
||||||
|
if (hashOnly) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
if (location.search) {
|
if (location.search) {
|
||||||
const currentETag = (
|
const currentETag = (
|
||||||
document.querySelector("meta[name=gomuks-frontend-etag]") as HTMLMetaElement
|
document.querySelector("meta[name=gomuks-frontend-etag]") as HTMLMetaElement
|
||||||
|
@ -164,7 +252,9 @@ const handleURLHash = (client: Client) => {
|
||||||
}
|
}
|
||||||
const state = JSON.parse(newURL.searchParams.get("state") || "{}")
|
const state = JSON.parse(newURL.searchParams.get("state") || "{}")
|
||||||
newURL.search = ""
|
newURL.search = ""
|
||||||
history.replaceState(state, "", newURL.toString())
|
// Set an extra empty state to ensure back button goes to room list instead of reloading the page.
|
||||||
|
history.replaceState({}, "", newURL.toString())
|
||||||
|
history.pushState(state, "")
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
return history.state
|
return history.state
|
||||||
|
@ -174,7 +264,7 @@ const handleURLHash = (client: Client) => {
|
||||||
const uri = parseMatrixURI(decodedURI)
|
const uri = parseMatrixURI(decodedURI)
|
||||||
if (!uri) {
|
if (!uri) {
|
||||||
console.error("Invalid matrix URI", decodedURI)
|
console.error("Invalid matrix URI", decodedURI)
|
||||||
return history.state
|
return hashOnly ? null : history.state
|
||||||
}
|
}
|
||||||
console.log("Handling URI", uri)
|
console.log("Handling URI", uri)
|
||||||
const newURL = new URL(location.href)
|
const newURL = new URL(location.href)
|
||||||
|
@ -190,47 +280,82 @@ const handleURLHash = (client: Client) => {
|
||||||
history.replaceState(newState, "", newURL.toString())
|
history.replaceState(newState, "", newURL.toString())
|
||||||
return newState
|
return newState
|
||||||
} else if (uri.identifier.startsWith("!")) {
|
} else if (uri.identifier.startsWith("!")) {
|
||||||
const newState = { room_id: uri.identifier }
|
const newState = { room_id: uri.identifier, source_via: uri.params.getAll("via") }
|
||||||
history.replaceState(newState, "", newURL.toString())
|
history.replaceState(newState, "", newURL.toString())
|
||||||
return newState
|
return newState
|
||||||
} else if (uri.identifier.startsWith("#")) {
|
} else if (uri.identifier.startsWith("#")) {
|
||||||
|
history.replaceState(history.state, "", newURL.toString())
|
||||||
// TODO loading indicator or something for this?
|
// TODO loading indicator or something for this?
|
||||||
client.rpc.resolveAlias(uri.identifier).then(
|
client.rpc.resolveAlias(uri.identifier).then(
|
||||||
res => {
|
res => {
|
||||||
history.pushState({ room_id: res.room_id }, "", newURL.toString())
|
context.setActiveRoom(res.room_id, {
|
||||||
|
alias: uri.identifier,
|
||||||
|
via: res.servers.slice(0, 3),
|
||||||
|
})
|
||||||
},
|
},
|
||||||
err => window.alert(`Failed to resolve room alias ${uri.identifier}: ${err}`),
|
err => window.alert(`Failed to resolve room alias ${uri.identifier}: ${err}`),
|
||||||
)
|
)
|
||||||
return null
|
return null
|
||||||
} else {
|
} else {
|
||||||
console.error("Invalid matrix URI", uri)
|
console.error("Invalid matrix URI", uri)
|
||||||
|
history.replaceState(history.state, "", newURL.toString())
|
||||||
|
}
|
||||||
|
return hashOnly ? null : history.state
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActiveRoomType = [RoomStateStore | RoomPreviewProps | null, RoomStateStore | RoomPreviewProps | null]
|
||||||
|
|
||||||
|
const activeRoomReducer = (
|
||||||
|
prev: ActiveRoomType,
|
||||||
|
active: RoomStateStore | RoomPreviewProps | "clear-animation" | null,
|
||||||
|
): ActiveRoomType => {
|
||||||
|
if (active === "clear-animation") {
|
||||||
|
return prev[1] === null ? [null, null] : prev
|
||||||
|
} else if (window.innerWidth > 720 || window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
|
||||||
|
return [null, active]
|
||||||
|
} else {
|
||||||
|
return [prev[1], active]
|
||||||
}
|
}
|
||||||
return history.state
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const MainScreen = () => {
|
const MainScreen = () => {
|
||||||
const [activeRoom, directSetActiveRoom] = useState<RoomStateStore | null>(null)
|
const [[prevActiveRoom, activeRoom], directSetActiveRoom] = useReducer(activeRoomReducer, [null, null])
|
||||||
|
const [space, directSetSpace] = useState<RoomListFilter | null>(null)
|
||||||
|
const skipNextTransitionRef = useRef(false)
|
||||||
const [rightPanel, directSetRightPanel] = useState<RightPanelProps | null>(null)
|
const [rightPanel, directSetRightPanel] = useState<RightPanelProps | null>(null)
|
||||||
const client = use(ClientContext)!
|
const client = use(ClientContext)!
|
||||||
const syncStatus = useEventAsState(client.syncStatus)
|
const syncStatus = useEventAsState(client.syncStatus)
|
||||||
const context = useMemo(
|
const context = useMemo(
|
||||||
() => new ContextFields(directSetRightPanel, directSetActiveRoom, client),
|
() => new ContextFields(directSetRightPanel, directSetActiveRoom, directSetSpace, client),
|
||||||
[client],
|
[client],
|
||||||
)
|
)
|
||||||
useLayoutEffect(() => {
|
|
||||||
window.mainScreenContext = context
|
|
||||||
}, [context])
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const listener = (evt: PopStateEvent) => {
|
window.mainScreenContext = context
|
||||||
|
const listener = (evt: Pick<PopStateEvent, "state" | "hasUAVisualTransition">) => {
|
||||||
|
skipNextTransitionRef.current = evt.hasUAVisualTransition
|
||||||
const roomID = evt.state?.room_id ?? null
|
const roomID = evt.state?.room_id ?? null
|
||||||
|
const spaceID = evt.state?.space_id ?? undefined
|
||||||
|
if (spaceID !== client.store.currentRoomListFilter?.id) {
|
||||||
|
context.setSpace(client.store.getSpaceByID(spaceID), false)
|
||||||
|
}
|
||||||
if (roomID !== client.store.activeRoomID) {
|
if (roomID !== client.store.activeRoomID) {
|
||||||
context.setActiveRoom(roomID, false)
|
context.setActiveRoom(roomID, {
|
||||||
|
alias: ensureString(evt.state?.source_alias) || undefined,
|
||||||
|
via: ensureStringArray(evt.state?.source_via),
|
||||||
|
}, undefined, false)
|
||||||
}
|
}
|
||||||
context.setRightPanel(evt.state?.right_panel ?? null, false)
|
context.setRightPanel(evt.state?.right_panel ?? null, false)
|
||||||
}
|
}
|
||||||
|
const hashListener = () => {
|
||||||
|
const state = handleURLHash(client, context, true)
|
||||||
|
if (state !== null) {
|
||||||
|
listener({ state, hasUAVisualTransition: false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener("hashchange", hashListener)
|
||||||
window.addEventListener("popstate", listener)
|
window.addEventListener("popstate", listener)
|
||||||
const initHandle = () => {
|
const initHandle = () => {
|
||||||
const state = handleURLHash(client)
|
const state = handleURLHash(client, context)
|
||||||
listener({ state } as PopStateEvent)
|
listener({ state } as PopStateEvent)
|
||||||
}
|
}
|
||||||
let cancel = () => {}
|
let cancel = () => {}
|
||||||
|
@ -241,33 +366,27 @@ const MainScreen = () => {
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("popstate", listener)
|
window.removeEventListener("popstate", listener)
|
||||||
|
window.removeEventListener("hashchange", hashListener)
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
}, [context, client])
|
}, [context, client])
|
||||||
useEffect(() => context.keybindings.listen(), [context])
|
useEffect(() => context.keybindings.listen(), [context])
|
||||||
useInsertionEffect(() => {
|
|
||||||
const styleTags = document.createElement("style")
|
|
||||||
styleTags.textContent = `
|
|
||||||
div.html-body > a.hicli-matrix-uri-user[href="matrix:u/${client.userID.slice(1).replaceAll(`"`, `\\"`)}"] {
|
|
||||||
background-color: var(--highlight-pill-background-color);
|
|
||||||
color: var(--highlight-pill-text-color);
|
|
||||||
}
|
|
||||||
`
|
|
||||||
document.head.appendChild(styleTags)
|
|
||||||
return () => {
|
|
||||||
document.head.removeChild(styleTags)
|
|
||||||
}
|
|
||||||
}, [client.userID])
|
|
||||||
const [roomListWidth, resizeHandle1] = useResizeHandle(
|
const [roomListWidth, resizeHandle1] = useResizeHandle(
|
||||||
300, 48, 900, "roomListWidth", { className: "room-list-resizer" },
|
350, 96, Math.min(900, window.innerWidth * 0.4),
|
||||||
|
"roomListWidth", { className: "room-list-resizer" },
|
||||||
)
|
)
|
||||||
const [rightPanelWidth, resizeHandle2] = useResizeHandle(
|
const [rightPanelWidth, resizeHandle2] = useResizeHandle(
|
||||||
300, 100, 900, "rightPanelWidth", { className: "right-panel-resizer", inverted: true },
|
300, 100, Math.min(900, window.innerWidth * 0.4),
|
||||||
|
"rightPanelWidth", { className: "right-panel-resizer", inverted: true },
|
||||||
)
|
)
|
||||||
const extraStyle = {
|
const extraStyle = {
|
||||||
["--room-list-width" as string]: `${roomListWidth}px`,
|
["--room-list-width" as string]: `${roomListWidth}px`,
|
||||||
["--right-panel-width" as string]: `${rightPanelWidth}px`,
|
["--right-panel-width" as string]: `${rightPanelWidth}px`,
|
||||||
}
|
}
|
||||||
|
if (skipNextTransitionRef.current) {
|
||||||
|
extraStyle["transition"] = "none"
|
||||||
|
skipNextTransitionRef.current = false
|
||||||
|
}
|
||||||
const classNames = ["matrix-main"]
|
const classNames = ["matrix-main"]
|
||||||
if (activeRoom) {
|
if (activeRoom) {
|
||||||
classNames.push("room-selected")
|
classNames.push("room-selected")
|
||||||
|
@ -282,27 +401,42 @@ const MainScreen = () => {
|
||||||
Waiting for first sync...
|
Waiting for first sync...
|
||||||
</div>
|
</div>
|
||||||
} else if (
|
} else if (
|
||||||
syncStatus.type === "errored"
|
syncStatus.type === "erroring"
|
||||||
&& (syncStatus.error_count > 2 || (syncStatus.last_sync ?? 0) + SYNC_ERROR_HIDE_DELAY < Date.now())
|
&& (syncStatus.error_count > 2 || (syncStatus.last_sync ?? 0) + SYNC_ERROR_HIDE_DELAY < Date.now())
|
||||||
) {
|
) {
|
||||||
syncLoader = <div className="sync-status errored" title={syncStatus.error}>
|
syncLoader = <div className="sync-status errored" title={syncStatus.error}>
|
||||||
<SyncLoader color="var(--error-color)"/>
|
<SyncLoader color="var(--error-color)"/>
|
||||||
Sync is failing
|
Sync is failing
|
||||||
</div>
|
</div>
|
||||||
|
} else if (syncStatus.type === "permanently-failed") {
|
||||||
|
syncLoader = <div className="sync-status errored" title={syncStatus.error}>
|
||||||
|
Sync failed permanently
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
|
const activeRealRoom = activeRoom instanceof RoomStateStore ? activeRoom : null
|
||||||
|
const renderedRoom = activeRoom ?? prevActiveRoom
|
||||||
|
useEffect(() => {
|
||||||
|
if (prevActiveRoom !== null && activeRoom === null) {
|
||||||
|
// Note: this timeout must match the one in MainScreen.css
|
||||||
|
const timeout = setTimeout(() => directSetActiveRoom("clear-animation"), 300)
|
||||||
|
return () => clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
}, [activeRoom, prevActiveRoom])
|
||||||
return <MainScreenContext value={context}>
|
return <MainScreenContext value={context}>
|
||||||
<ModalWrapper>
|
<ModalWrapper>
|
||||||
<StylePreferences client={client} activeRoom={activeRoom}/>
|
<StylePreferences client={client} activeRoom={activeRealRoom}/>
|
||||||
<main className={classNames.join(" ")} style={extraStyle}>
|
<main className={classNames.join(" ")} style={extraStyle}>
|
||||||
<RoomList activeRoomID={activeRoom?.roomID ?? null}/>
|
<RoomList activeRoomID={activeRoom?.roomID ?? null} space={space}/>
|
||||||
{resizeHandle1}
|
{resizeHandle1}
|
||||||
{activeRoom
|
{renderedRoom
|
||||||
? <RoomView
|
? renderedRoom instanceof RoomStateStore
|
||||||
key={activeRoom.roomID}
|
? <RoomView
|
||||||
room={activeRoom}
|
key={renderedRoom.roomID}
|
||||||
rightPanel={rightPanel}
|
room={renderedRoom}
|
||||||
rightPanelResizeHandle={resizeHandle2}
|
rightPanel={rightPanel}
|
||||||
/>
|
rightPanelResizeHandle={resizeHandle2}
|
||||||
|
/>
|
||||||
|
: <RoomPreview {...renderedRoom} />
|
||||||
: rightPanel && <>
|
: rightPanel && <>
|
||||||
<div className="room-view placeholder"/>
|
<div className="room-view placeholder"/>
|
||||||
{resizeHandle2}
|
{resizeHandle2}
|
||||||
|
|
|
@ -13,12 +13,15 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import { createContext } from "react"
|
import React, { createContext } from "react"
|
||||||
|
import { RoomListFilter } from "@/api/statestore"
|
||||||
import type { RoomID } from "@/api/types"
|
import type { RoomID } from "@/api/types"
|
||||||
import type { RightPanelProps } from "./rightpanel/RightPanel.tsx"
|
import type { RightPanelProps } from "./rightpanel/RightPanel.tsx"
|
||||||
|
import type { RoomPreviewProps } from "./roomview/RoomPreview.tsx"
|
||||||
|
|
||||||
export interface MainScreenContextFields {
|
export interface MainScreenContextFields {
|
||||||
setActiveRoom: (roomID: RoomID | null) => void
|
setActiveRoom: (roomID: RoomID | null, previewMeta?: Partial<RoomPreviewProps>, toSpace?: RoomListFilter) => void
|
||||||
|
setSpace: (space: RoomListFilter | null, pushState?: boolean) => void
|
||||||
clickRoom: (evt: React.MouseEvent) => void
|
clickRoom: (evt: React.MouseEvent) => void
|
||||||
clearActiveRoom: () => void
|
clearActiveRoom: () => void
|
||||||
|
|
||||||
|
@ -31,6 +34,9 @@ const stubContext = {
|
||||||
get setActiveRoom(): never {
|
get setActiveRoom(): never {
|
||||||
throw new Error("MainScreenContext used outside main screen")
|
throw new Error("MainScreenContext used outside main screen")
|
||||||
},
|
},
|
||||||
|
get setSpace(): never {
|
||||||
|
throw new Error("MainScreenContext used outside main screen")
|
||||||
|
},
|
||||||
get clickRoom(): never {
|
get clickRoom(): never {
|
||||||
throw new Error("MainScreenContext used outside main screen")
|
throw new Error("MainScreenContext used outside main screen")
|
||||||
},
|
},
|
||||||
|
|