diff --git a/package-lock.json b/package-lock.json
index f35e808f05..12ad1728b8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
"dependencies": {
"@mdi/js": "^7.4.47",
"@sapphi-red/web-noise-suppressor": "^0.3.5",
+ "@shiguredo/virtual-background": "^2023.2.0",
"@traptitech/traq": "^3.17.0-3",
"@traptitech/traq-markdown-it": "^6.3.0",
"autosize": "^6.0.1",
@@ -20,7 +21,9 @@
"firebase": "^11.2.0",
"highlight.js": "^11.11.1",
"idb-keyval": "^6.2.0",
+ "livekit-client": "^2.8.0",
"mitt": "^3.0.0",
+ "party-js": "^2.2.0",
"skyway-js": "^4.4.5",
"text-field-edit": "^4.1.1",
"throttle-debounce": "^5.0.2",
@@ -1664,6 +1667,11 @@
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
"dev": true
},
+ "node_modules/@bufbuild/protobuf": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.0.tgz",
+ "integrity": "sha512-QDdVFLoN93Zjg36NoQPZfsVH9tZew7wKDKyV5qRdj8ntT4wQCOradQjRaTdwMhWUYsgKsvCINKKm87FdEk96Ag=="
+ },
"node_modules/@colors/colors": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
@@ -3220,6 +3228,19 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@livekit/mutex": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@livekit/mutex/-/mutex-1.1.1.tgz",
+ "integrity": "sha512-EsshAucklmpuUAfkABPxJNhzj9v2sG7JuzFDL4ML1oJQSV14sqrpTYnsaOudMAw9yOaW53NU3QQTlUQoRs4czw=="
+ },
+ "node_modules/@livekit/protocol": {
+ "version": "1.30.0",
+ "resolved": "https://registry.npmjs.org/@livekit/protocol/-/protocol-1.30.0.tgz",
+ "integrity": "sha512-SDI9ShVKj8N3oOSinr8inaxD3FXgmgoJlqN35uU/Yx1sdoDeQbzAuBFox7bYjM+VhnZ1V22ivIDjAsKr00H+XQ==",
+ "dependencies": {
+ "@bufbuild/protobuf": "^1.10.0"
+ }
+ },
"node_modules/@mdi/js": {
"version": "7.4.47",
"resolved": "https://registry.npmjs.org/@mdi/js/-/js-7.4.47.tgz",
@@ -3997,6 +4018,14 @@
"resolved": "https://registry.npmjs.org/@sapphi-red/web-noise-suppressor/-/web-noise-suppressor-0.3.5.tgz",
"integrity": "sha512-jh3+V9yM+zxLriQexoGm0GatoPaJWjs6ypFIbFYwQp+AoUb55eUXrjKtKQyuC5zShzzeAQUl0M5JzqB7SSrsRA=="
},
+ "node_modules/@shiguredo/virtual-background": {
+ "version": "2023.2.0",
+ "resolved": "https://registry.npmjs.org/@shiguredo/virtual-background/-/virtual-background-2023.2.0.tgz",
+ "integrity": "sha512-nvJAsf29ThXMN7tEvlvZ45MeIoXW2uoJxvBwsesYInjoSy0RASQr7qxrLMglX2yyztOeYZqJknQBttjJTGH/QA==",
+ "dependencies": {
+ "@types/dom-mediacapture-transform": "^0.1.4"
+ }
+ },
"node_modules/@sideway/address": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
@@ -4121,12 +4150,25 @@
"integrity": "sha512-o0ZyU3ePp3+KRbhHsY4ogjc+ZQWgVN5h6j8BHW5RII4cFKi6PEKK9QPAcphJVkD0dGpyFnD3VRR0WMvHVjCv9w==",
"dev": true
},
+ "node_modules/@types/dom-mediacapture-transform": {
+ "version": "0.1.10",
+ "resolved": "https://registry.npmjs.org/@types/dom-mediacapture-transform/-/dom-mediacapture-transform-0.1.10.tgz",
+ "integrity": "sha512-zUxMN2iShu7p3Fz5sqfvLp93qW/3sLs+RwXWWOkMb969hsuoVqUUokqrENjXqTMNmEEcVXKoHuMMbIGcWyrVVA==",
+ "dependencies": {
+ "@types/dom-webcodecs": "*"
+ }
+ },
"node_modules/@types/dom-screen-wake-lock": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@types/dom-screen-wake-lock/-/dom-screen-wake-lock-1.0.3.tgz",
"integrity": "sha512-3Iten7X3Zgwvk6kh6/NRdwN7WbZ760YgFCsF5AxDifltUQzW1RaW+WRmcVtgwFzLjaNu64H+0MPJ13yRa8g3Dw==",
"dev": true
},
+ "node_modules/@types/dom-webcodecs": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/@types/dom-webcodecs/-/dom-webcodecs-0.1.13.tgz",
+ "integrity": "sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ=="
+ },
"node_modules/@types/estree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
@@ -8777,6 +8819,22 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
+ "node_modules/livekit-client": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/livekit-client/-/livekit-client-2.8.0.tgz",
+ "integrity": "sha512-8/IXhacAFYdXMU1wFyc8/MSGCzHr02Hn9T5o3MX19TR03RHSaBKBF2xK8fQFINBmpcYkiMAnQL0P6K3nfcifQA==",
+ "dependencies": {
+ "@livekit/mutex": "1.1.1",
+ "@livekit/protocol": "1.30.0",
+ "events": "^3.3.0",
+ "loglevel": "^1.8.0",
+ "sdp-transform": "^2.14.1",
+ "ts-debounce": "^4.0.0",
+ "tslib": "2.8.1",
+ "typed-emitter": "^2.1.0",
+ "webrtc-adapter": "^9.0.0"
+ }
+ },
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -8892,6 +8950,18 @@
"node": ">=8"
}
},
+ "node_modules/loglevel": {
+ "version": "1.9.2",
+ "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz",
+ "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==",
+ "engines": {
+ "node": ">= 0.6.0"
+ },
+ "funding": {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/loglevel"
+ }
+ },
"node_modules/long": {
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/long/-/long-5.2.4.tgz",
@@ -9412,6 +9482,11 @@
"resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz",
"integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow=="
},
+ "node_modules/party-js": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/party-js/-/party-js-2.2.0.tgz",
+ "integrity": "sha512-50hGuALCpvDTrQLPQ1fgUgxKIWAH28ShVkmeK/3zhO0YJyCqkhrZhQEkWPxDYLvbFJ7YAXyROmFEu35gKpZLtQ=="
+ },
"node_modules/patch-package": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.0.tgz",
@@ -10163,7 +10238,7 @@
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
- "dev": true,
+ "devOptional": true,
"dependencies": {
"tslib": "^2.1.0"
}
@@ -10260,6 +10335,11 @@
"node": ">=v12.22.7"
}
},
+ "node_modules/sdp": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.0.tgz",
+ "integrity": "sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw=="
+ },
"node_modules/sdp-transform": {
"version": "2.14.2",
"resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.14.2.tgz",
@@ -11213,6 +11293,11 @@
"typescript": ">=4.8.4"
}
},
+ "node_modules/ts-debounce": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/ts-debounce/-/ts-debounce-4.0.0.tgz",
+ "integrity": "sha512-+1iDGY6NmOGidq7i7xZGA4cm8DAa6fqdYcvO5Z6yBevH++Bdo9Qt/mN0TzHUgcCcKv1gmh9+W5dHqz8pMWbCbg=="
+ },
"node_modules/ts-pattern": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/ts-pattern/-/ts-pattern-5.6.1.tgz",
@@ -11360,6 +11445,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/typed-emitter": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/typed-emitter/-/typed-emitter-2.1.0.tgz",
+ "integrity": "sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==",
+ "optionalDependencies": {
+ "rxjs": "*"
+ }
+ },
"node_modules/typescript": {
"version": "5.7.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
@@ -12987,6 +13080,18 @@
"node": ">=12"
}
},
+ "node_modules/webrtc-adapter": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-9.0.1.tgz",
+ "integrity": "sha512-1AQO+d4ElfVSXyzNVTOewgGT/tAomwwztX/6e3totvyyzXPvXIIuUUjAmyZGbKBKbZOXauuJooZm3g6IuFuiNQ==",
+ "dependencies": {
+ "sdp": "^3.2.0"
+ },
+ "engines": {
+ "node": ">=6.0.0",
+ "npm": ">=3.10.0"
+ }
+ },
"node_modules/websocket-driver": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz",
diff --git a/package.json b/package.json
index 14289a448f..27cfe22ca8 100644
--- a/package.json
+++ b/package.json
@@ -28,6 +28,7 @@
"dependencies": {
"@mdi/js": "^7.4.47",
"@sapphi-red/web-noise-suppressor": "^0.3.5",
+ "@shiguredo/virtual-background": "^2023.2.0",
"@traptitech/traq": "^3.17.0-3",
"@traptitech/traq-markdown-it": "^6.3.0",
"autosize": "^6.0.1",
@@ -37,7 +38,9 @@
"firebase": "^11.2.0",
"highlight.js": "^11.11.1",
"idb-keyval": "^6.2.0",
+ "livekit-client": "^2.8.0",
"mitt": "^3.0.0",
+ "party-js": "^2.2.0",
"skyway-js": "^4.4.5",
"text-field-edit": "^4.1.1",
"throttle-debounce": "^5.0.2",
@@ -51,8 +54,8 @@
"zod": "^3.24.1"
},
"devDependencies": {
- "@stylistic/eslint-plugin-ts": "^2.13.0",
"@pinia/testing": "^0.1.6",
+ "@stylistic/eslint-plugin-ts": "^2.13.0",
"@types/autosize": "^4.0.3",
"@types/dom-screen-wake-lock": "^1.0.3",
"@types/katex": "^0.16.7",
@@ -63,9 +66,9 @@
"@types/throttle-debounce": "^5.0.2",
"@types/turndown": "^5.0.5",
"@types/vue-select": "^3.16.8",
+ "@types/webappsec-credential-management": "^0.6.9",
"@typescript-eslint/eslint-plugin": "^8.20.0",
"@typescript-eslint/parser": "^8.20.0",
- "@types/webappsec-credential-management": "^0.6.9",
"@typescript/lib-dom": "npm:@types/web@^0.0.72",
"@vitejs/plugin-vue": "^5.2.1",
"@vitest/coverage-v8": "^2.1.8",
@@ -73,14 +76,14 @@
"autoprefixer": "^10.4.20",
"axios": "^1.7.9",
"browserslist": "^4.24.4",
+ "cypress": "^13.17.0",
+ "esbuild": "^0.24.2",
+ "esbuild-plugin-browserslist": "^0.15.0",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-cypress": "^4.1.0",
"eslint-plugin-unused-imports": "^4.1.4",
"eslint-plugin-vue": "^9.32.0",
- "cypress": "^13.17.0",
- "esbuild": "^0.24.2",
- "esbuild-plugin-browserslist": "^0.15.0",
"fonteditor-core": "^2.4.1",
"jsdom": "^26.0.0",
"patch-package": "^8.0.0",
diff --git a/src/App.vue b/src/App.vue
index f3a67b06cb..ce16092de2 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -17,14 +17,15 @@ import useHtmlDataset from '/@/composables/document/useHtmlDataset'
import { useThemeVariables } from '/@/composables/document/useThemeVariables'
import { useResponsiveStore } from '/@/store/ui/responsive'
import { useBrowserSettings } from '/@/store/app/browserSettings'
-import { useAppRtcStore } from '/@/store/app/rtc'
import { useTts } from '/@/store/app/tts'
import { useThemeSettings } from '/@/store/app/themeSettings'
import useDocumentTitle from '/@/composables/document/useDocumentTitle'
const useQallConfirmer = () => {
- const { isCurrentDevice } = useAppRtcStore()
window.addEventListener('beforeunload', event => {
+ // TODO: Qall
+ // ここは適切な変数を置く
+ const isCurrentDevice = computed(() => false)
if (isCurrentDevice.value) {
const unloadMessage = 'Qall中ですが本当に終了しますか?'
event.preventDefault()
diff --git a/src/assets/icons/add_reaction.svg b/src/assets/icons/add_reaction.svg
new file mode 100644
index 0000000000..f93c6d661f
--- /dev/null
+++ b/src/assets/icons/add_reaction.svg
@@ -0,0 +1,8 @@
+
diff --git a/src/assets/icons/call_end.svg b/src/assets/icons/call_end.svg
new file mode 100644
index 0000000000..f70c5d3a3a
--- /dev/null
+++ b/src/assets/icons/call_end.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/icons/group_qall.svg b/src/assets/icons/group_qall.svg
new file mode 100644
index 0000000000..903365b189
--- /dev/null
+++ b/src/assets/icons/group_qall.svg
@@ -0,0 +1,8 @@
+
diff --git a/src/assets/icons/mic.svg b/src/assets/icons/mic.svg
new file mode 100644
index 0000000000..ac0583091f
--- /dev/null
+++ b/src/assets/icons/mic.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/assets/icons/mic_off.svg b/src/assets/icons/mic_off.svg
new file mode 100644
index 0000000000..1d544dd91f
--- /dev/null
+++ b/src/assets/icons/mic_off.svg
@@ -0,0 +1,8 @@
+
diff --git a/src/assets/icons/screen-share.svg b/src/assets/icons/screen-share.svg
new file mode 100644
index 0000000000..d3d66c0b50
--- /dev/null
+++ b/src/assets/icons/screen-share.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/icons/sound_detection_loud_sound.svg b/src/assets/icons/sound_detection_loud_sound.svg
new file mode 100644
index 0000000000..b506f23f79
--- /dev/null
+++ b/src/assets/icons/sound_detection_loud_sound.svg
@@ -0,0 +1,8 @@
+
diff --git a/src/assets/icons/stop-screen-share.svg b/src/assets/icons/stop-screen-share.svg
new file mode 100644
index 0000000000..6949cc3ce0
--- /dev/null
+++ b/src/assets/icons/stop-screen-share.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/icons/videocam.svg b/src/assets/icons/videocam.svg
new file mode 100644
index 0000000000..c267aa9bf4
--- /dev/null
+++ b/src/assets/icons/videocam.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/assets/icons/videocam_off.svg b/src/assets/icons/videocam_off.svg
new file mode 100644
index 0000000000..e93196c843
--- /dev/null
+++ b/src/assets/icons/videocam_off.svg
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/src/assets/mdi.ts b/src/assets/mdi.ts
index 2130c677b9..05e7af93f6 100644
--- a/src/assets/mdi.ts
+++ b/src/assets/mdi.ts
@@ -24,18 +24,23 @@ import {
mdiEmail,
mdiTag,
mdiPlus,
+ mdiPlusCircle,
mdiMagnify,
mdiHistory,
mdiDownload,
mdiEmoticonOutline,
mdiCog,
mdiAccount,
+ mdiAccountMultiple,
+ mdiAccountMinus,
mdiCogs,
mdiBrightness6,
mdiPencilOutline,
mdiToggleSwitchOff,
mdiToggleSwitch,
mdiChevronDoubleLeft,
+ mdiChevronDoubleUp,
+ mdiChevronDoubleDown,
mdiChevronLeft,
mdiChevronRight,
mdiBookmark,
@@ -80,7 +85,11 @@ import {
mdiFormatTitle,
mdiCloseCircle,
mdiNotebook,
- mdiDelete
+ mdiDelete,
+ mdiVideo,
+ mdiVideoOff,
+ mdiCommentTextMultipleOutline,
+ mdiCommentOffOutline
} from '@mdi/js'
interface MdiIconsMapping {
@@ -117,10 +126,13 @@ const mdi: MdiIconsMapping = {
tags: mdiTagMultiple,
email: mdiEmail,
plus: mdiPlus,
+ 'plus-circle': mdiPlusCircle,
download: mdiDownload,
'emoticon-outline': mdiEmoticonOutline,
cog: mdiCog,
account: mdiAccount,
+ 'account-multiple': mdiAccountMultiple,
+ 'account-minus': mdiAccountMinus,
cogs: mdiCogs,
'brightness-6': mdiBrightness6,
pencil: mdiPencil,
@@ -128,6 +140,8 @@ const mdi: MdiIconsMapping = {
'toggle-switch-off': mdiToggleSwitchOff,
'toggle-switch-on': mdiToggleSwitch,
'chevron-double': mdiChevronDoubleLeft,
+ 'chevron-double-up': mdiChevronDoubleUp,
+ 'chevron-double-down': mdiChevronDoubleDown,
'chevron-left': mdiChevronLeft,
'chevron-right': mdiChevronRight,
'chevron-up': mdiChevronUp,
@@ -169,7 +183,11 @@ const mdi: MdiIconsMapping = {
stop: mdiStop,
crown: mdiCrown,
'format-title': mdiFormatTitle,
- delete: mdiDelete
+ delete: mdiDelete,
+ video: mdiVideo,
+ 'video-off': mdiVideoOff,
+ 'comment-outline': mdiCommentTextMultipleOutline,
+ 'comment-off-outline': mdiCommentOffOutline
}
export default mdi
diff --git a/src/components/Main/MainView/ChannelView/ChannelHeader/ChannelHeaderToolsList.vue b/src/components/Main/MainView/ChannelView/ChannelHeader/ChannelHeaderToolsList.vue
index 09b79d83ea..e7fe77bc34 100644
--- a/src/components/Main/MainView/ChannelView/ChannelHeader/ChannelHeaderToolsList.vue
+++ b/src/components/Main/MainView/ChannelView/ChannelHeader/ChannelHeaderToolsList.vue
@@ -2,15 +2,15 @@
callingChannel.value === props.channelId)
+const disabled = computed(() => !!callingChannel.value && !isCallingHere.value)
const { changeToNextSubscriptionLevel, currentChannelSubscription } =
useChannelSubscriptionState(toRef(props, 'channelId'))
diff --git a/src/components/Main/MainView/ChannelView/ChannelHeader/ChannelHeaderToolsMenu.vue b/src/components/Main/MainView/ChannelView/ChannelHeader/ChannelHeaderToolsMenu.vue
index 9594bd6f41..f5d7c77b03 100644
--- a/src/components/Main/MainView/ChannelView/ChannelHeader/ChannelHeaderToolsMenu.vue
+++ b/src/components/Main/MainView/ChannelView/ChannelHeader/ChannelHeaderToolsMenu.vue
@@ -1,14 +1,14 @@
+ joinQall(props.channelId, true)"
+ @click-item="emit('clickItem')"
+ />
@@ -58,7 +65,7 @@ import { UserPermission } from '@traptitech/traq'
import { useMeStore } from '/@/store/domain/me'
import PrimaryViewHeaderPopupFrame from '/@/components/Main/MainView/PrimaryViewHeader/PrimaryViewHeaderPopupFrame.vue'
import HeaderToolsMenuItem from '/@/components/Main/MainView/PrimaryViewHeader/PrimaryViewHeaderPopupMenuItem.vue'
-import useQall from './composables/useQall'
+import { useQall } from '/@/composables/qall/useQall'
import type { ChannelId } from '/@/types/entity-ids'
import useChannelCreateModal from './composables/useChannelCreateModal'
import useNotificationModal from './composables/useNotificationModal'
@@ -84,14 +91,9 @@ const props = withDefaults(
const { isMobile } = useResponsiveStore()
-const {
- isQallFeatureEnabled,
- isQallSessionOpened,
- canToggleQall,
- qallIconName,
- qallLabel,
- toggleQall
-} = useQall(props)
+const { joinQall, callingChannel } = useQall()
+const isCallingHere = computed(() => callingChannel.value === props.channelId)
+const disabled = computed(() => !!callingChannel.value && !isCallingHere.value)
const { isChildChannelCreatable, openChannelCreateModal } =
useChannelCreateModal(props)
diff --git a/src/components/Main/MainView/ChannelView/ChannelHeader/composables/useQall.ts b/src/components/Main/MainView/ChannelView/ChannelHeader/composables/useQall.ts
deleted file mode 100644
index 0c665d5fa8..0000000000
--- a/src/components/Main/MainView/ChannelView/ChannelHeader/composables/useQall.ts
+++ /dev/null
@@ -1,104 +0,0 @@
-import { computed } from 'vue'
-import type { ChannelId } from '/@/types/entity-ids'
-import { useToastStore } from '/@/store/ui/toast'
-import { useAppRtcStore } from '/@/store/app/rtc'
-import { useDomainRtcStore } from '/@/store/domain/rtc'
-import { useRtcSettings } from '/@/store/app/rtcSettings'
-
-const isSkywayApikeySet = window.traQConfig.skyway !== undefined
-
-const useQall = (props: { channelId: ChannelId; isArchived: boolean }) => {
- const { isEnabled: isRtcEnabled } = useRtcSettings()
- const {
- /** この端末でチャンネル問わず自分がQallに参加している */
- isCurrentDevice: isJoinedWithCurrentDevice,
- startQall,
- endQall
- } = useAppRtcStore()
- const { sessionInfoMap, qallSession, currentRTCState } = useDomainRtcStore()
- const { addErrorToast } = useToastStore()
-
- const isQallFeatureEnabled = computed(
- () => isSkywayApikeySet && isRtcEnabled.value
- )
-
- /** このチャンネルでQallが開始されている */
- const isQallSessionOpened = computed(() =>
- [...sessionInfoMap.value.values()].some(
- s => s?.channelId === props.channelId && s?.type === 'qall'
- )
- )
- /** 端末・チャンネルを問わず自分がQallに参加している */
- const hasActiveQallSession = computed(() => !!qallSession.value)
- /** 端末問わず自分がこのチャンネルのQallに参加している */
- const isJoinedQallSession = computed(
- () =>
- hasActiveQallSession.value &&
- currentRTCState.value?.channelId === props.channelId
- )
-
- /**
- * Qallが開始できるのは、このチャンネルがアーカイブされていなく
- * どの端末でもどのチャンネルにもQallに参加していないとき
- */
- const canStartQall = computed(
- () => !props.isArchived && !hasActiveQallSession.value
- )
- /**
- * Qallが終了できるのは、
- * 自分がこの端末でこのチャンネルのQallに参加しているとき
- */
- const canEndQall = computed(
- () => isJoinedQallSession.value && isJoinedWithCurrentDevice.value
- )
- const canToggleQall = computed(() => canStartQall.value || canEndQall.value)
-
- const qallIconName = computed(() =>
- isJoinedQallSession.value ? 'phone' : 'phone-outline'
- )
- const qallLabel = computed(() => {
- if (isQallSessionOpened.value) {
- if (isJoinedWithCurrentDevice.value) {
- return 'Qallを終了'
- }
- if (isJoinedQallSession.value) {
- return '別のデバイスでQall中'
- }
- return 'Qallに参加'
- }
- if (hasActiveQallSession.value) {
- return '他チャンネルでQall中'
- }
- return 'Qallを開始'
- })
-
- const startQallOnCurrentChannel = async () => {
- try {
- await startQall(props.channelId)
- } catch (e) {
- // eslint-disable-next-line no-console
- console.error('Qallの開始に失敗しました', e)
-
- addErrorToast('Qallの開始に失敗しました')
- }
- }
- const toggleQall = () => {
- if (isJoinedQallSession.value) {
- if (isJoinedWithCurrentDevice.value) {
- endQall()
- }
- } else {
- startQallOnCurrentChannel()
- }
- }
- return {
- isQallFeatureEnabled,
- isQallSessionOpened,
- canEndQall,
- canToggleQall,
- qallIconName,
- qallLabel,
- toggleQall
- }
-}
-export default useQall
diff --git a/src/components/Main/MainView/ChannelView/ChannelSidebar/ChannelSidebarContent.vue b/src/components/Main/MainView/ChannelView/ChannelSidebar/ChannelSidebarContent.vue
index 8da728d95a..08fea5d80b 100644
--- a/src/components/Main/MainView/ChannelView/ChannelSidebar/ChannelSidebarContent.vue
+++ b/src/components/Main/MainView/ChannelView/ChannelSidebar/ChannelSidebarContent.vue
@@ -49,8 +49,9 @@ import ChannelSidebarRelation from './ChannelSidebarRelation.vue'
import ChannelSidebarQall from './ChannelSidebarQall.vue'
import ChannelSidebarBots from './ChannelSidebarBots.vue'
import type { UserId, ChannelId } from '/@/types/entity-ids'
-import { useQallSession } from './composables/useChannelRTCSession'
import { useModelSyncer } from '/@/composables/useModelSyncer'
+import { useQall } from '/@/composables/qall/useQall'
+import { computed } from 'vue'
const props = withDefaults(
defineProps<{
@@ -70,7 +71,14 @@ const emit = defineEmits<{
(e: 'update:isViewersDetailOpen', value: boolean): void
}>()
-const { sessionUserIds: qallUserIds } = useQallSession(props)
+const { rooms: roomWithParticipants } = useQall()
+
+const qallUserIds = computed(
+ () =>
+ roomWithParticipants.value
+ .find(room => room.channel.id === props.channelId)
+ ?.participants?.map(participant => participant.user.id) ?? []
+)
const isViewersDetailOpen = useModelSyncer(props, emit, 'isViewersDetailOpen')
diff --git a/src/components/Main/MainView/ChannelView/ChannelSidebar/composables/useChannelRTCSession.ts b/src/components/Main/MainView/ChannelView/ChannelSidebar/composables/useChannelRTCSession.ts
deleted file mode 100644
index 4de13bfcd7..0000000000
--- a/src/components/Main/MainView/ChannelView/ChannelSidebar/composables/useChannelRTCSession.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import type { ChannelId } from '/@/types/entity-ids'
-import { computed } from 'vue'
-import type { SessionType } from '/@/store/domain/rtc'
-import { useDomainRtcStore } from '/@/store/domain/rtc'
-
-const useRTCSession =
- (sessionType: SessionType) => (props: { channelId: ChannelId }) => {
- const { userStateMap, getChannelRTCSessionId } = useDomainRtcStore()
-
- const sessionId = computed(() =>
- getChannelRTCSessionId(sessionType, props.channelId)
- )
- const sessionUserIds = computed(() =>
- [...userStateMap.value.entries()]
- .filter(([_, userState]) =>
- userState.sessionStates.some(
- sessionState => sessionState.sessionId === sessionId.value
- )
- )
- .map(([userId, _]) => userId)
- )
- return { sessionId, sessionUserIds }
- }
-
-export const useQallSession = useRTCSession('qall')
diff --git a/src/components/Main/MainView/ChannelView/ChannelView.vue b/src/components/Main/MainView/ChannelView/ChannelView.vue
index b2023ce6ef..d04e38d9b7 100644
--- a/src/components/Main/MainView/ChannelView/ChannelView.vue
+++ b/src/components/Main/MainView/ChannelView/ChannelView.vue
@@ -4,7 +4,10 @@
+
+
diff --git a/src/components/Main/MainView/ChannelView/ChannelViewContent/composables/useChannelView.ts b/src/components/Main/MainView/ChannelView/ChannelViewContent/composables/useChannelView.ts
new file mode 100644
index 0000000000..2258c21b61
--- /dev/null
+++ b/src/components/Main/MainView/ChannelView/ChannelViewContent/composables/useChannelView.ts
@@ -0,0 +1,71 @@
+import { computed, ref, type ShallowRef } from 'vue'
+import useChannelMessageFetcher from './useChannelMessageFetcher'
+import { useChannelsStore } from '/@/store/entities/channels'
+import { useSubscriptionStore } from '/@/store/domain/subscription'
+export const useChannelView = ({
+ channelId,
+ entryMessageId,
+ scrollerEle
+}: {
+ channelId: string
+ entryMessageId?: string
+ scrollerEle: ShallowRef<{ $el: HTMLDivElement } | undefined>
+}) => {
+ const isMessageShow = ref(false)
+
+ const {
+ messageIds,
+ isReachedEnd,
+ isReachedLatest,
+ isLoading,
+ lastLoadingDirection,
+ onLoadFormerMessagesRequest,
+ onLoadLatterMessagesRequest
+ } = useChannelMessageFetcher(scrollerEle, { channelId, entryMessageId })
+
+ const { channelsMap } = useChannelsStore()
+ const isArchived = computed(
+ () => channelsMap.value.get(channelId)?.archived ?? false
+ )
+
+ const { unreadChannelsMap } = useSubscriptionStore()
+ const resetIsReachedLatest = () => {
+ if (!unreadChannelsMap.value.get(channelId)) return
+ isReachedLatest.value = false
+ }
+
+ const showToNewMessageButton = ref(false)
+ const toNewMessage = (behavior?: ScrollBehavior) => {
+ if (!scrollerEle.value) return
+ showToNewMessageButton.value = false
+ scrollerEle.value.$el.scrollTo({
+ top: scrollerEle.value.$el.scrollHeight,
+ behavior: behavior
+ })
+ }
+
+ const handleScroll = () => {
+ if (scrollerEle.value === undefined || isLoading.value) return
+ const { scrollTop, scrollHeight, clientHeight } = scrollerEle.value.$el
+ showToNewMessageButton.value = scrollHeight - 2 * clientHeight > scrollTop
+ if (!isReachedLatest.value) {
+ showToNewMessageButton.value = true
+ }
+ }
+
+ return {
+ isMessageShow,
+ messageIds,
+ isReachedEnd,
+ isReachedLatest,
+ isLoading,
+ lastLoadingDirection,
+ onLoadFormerMessagesRequest,
+ onLoadLatterMessagesRequest,
+ isArchived,
+ resetIsReachedLatest,
+ showToNewMessageButton,
+ toNewMessage,
+ handleScroll
+ }
+}
diff --git a/src/components/Main/MainView/MessageInput/MessageInput.vue b/src/components/Main/MainView/MessageInput/MessageInput.vue
index 711a50d5d7..818b87cd76 100644
--- a/src/components/Main/MainView/MessageInput/MessageInput.vue
+++ b/src/components/Main/MainView/MessageInput/MessageInput.vue
@@ -2,7 +2,7 @@
@@ -42,19 +42,18 @@ import ChannelTree from '/@/components/Main/NavigationBar/ChannelList/ChannelTre
import NavigationContentContainer from '/@/components/Main/NavigationBar/NavigationContentContainer.vue'
import DMChannelList from '/@/components/Main/NavigationBar/DMChannelList/DMChannelList.vue'
import { computed, toRaw } from 'vue'
-import { isDefined } from '/@/lib/basic/array'
import { constructTreeFromIds } from '/@/lib/channelTree'
import { useChannelTree } from '/@/store/domain/channelTree'
-import { useDomainRtcStore } from '/@/store/domain/rtc'
import { useMeStore } from '/@/store/domain/me'
import { useChannelsStore } from '/@/store/entities/channels'
import useChannelsWithNotification from '/@/composables/subscription/useChannelsWithNotification'
import { filterTrees } from '/@/lib/basic/tree'
+import { useQall } from '/@/composables/qall/useQall'
const { homeChannelTree } = useChannelTree()
-const { channelSessionsMap } = useDomainRtcStore()
const { detail } = useMeStore()
const { channelsMap } = useChannelsStore()
+const { rooms: roomWithParticipants } = useQall()
const homeChannelWithTree = computed(() => {
if (!detail.value?.homeChannel) return []
@@ -73,11 +72,9 @@ const topLevelChannels = computed(() =>
// filterTreesは重いのと内部ではreactiveである必要がないのでtoRawする
filterTrees(toRaw(homeChannelTree.value.children), node => !node.archived)
)
-const channelsWithRtc = computed(() =>
- [...channelSessionsMap.value.entries()]
- .filter(([, sessionIds]) => sessionIds.size > 0)
- .map(([channelId]) => channelsMap.value.get(channelId))
- .filter(isDefined)
+
+const qallingChannels = computed(() =>
+ roomWithParticipants.value.map(room => room.channel)
)
diff --git a/src/components/Main/NavigationBar/composables/useNavigationSelectorEntry.ts b/src/components/Main/NavigationBar/composables/useNavigationSelectorEntry.ts
index b69e818e0e..842c02a2d8 100644
--- a/src/components/Main/NavigationBar/composables/useNavigationSelectorEntry.ts
+++ b/src/components/Main/NavigationBar/composables/useNavigationSelectorEntry.ts
@@ -7,9 +7,10 @@ import type { ThemeClaim } from '/@/lib/styles'
import { isDefined } from '/@/lib/basic/array'
import { useMessageInputStateStore } from '/@/store/ui/messageInputStateStore'
import { useAudioController } from '/@/store/ui/audioController'
-import { useAppRtcStore } from '/@/store/app/rtc'
import { useChannelsStore } from '/@/store/entities/channels'
import { useSubscriptionStore } from '/@/store/domain/subscription'
+import { useQall } from '/@/composables/qall/useQall'
+import { useMainViewStore } from '/@/store/ui/mainView'
export type NavigationSelectorEntry = {
type: NavigationItemType
@@ -79,11 +80,12 @@ export const ephemeralItems: Record<
}
const useNavigationSelectorEntry = () => {
- const { isCurrentDevice: hasActiveQallSession } = useAppRtcStore()
const { unreadChannelsMap } = useSubscriptionStore()
const { channelsMap, dmChannelsMap } = useChannelsStore()
const { hasInputChannel } = useMessageInputStateStore()
const { fileId } = useAudioController()
+ const { getQallingState } = useQall()
+ const { primaryView } = useMainViewStore()
const unreadChannels = computed(() => [...unreadChannelsMap.value.values()])
const notificationState = reactive({
@@ -96,6 +98,11 @@ const useNavigationSelectorEntry = () => {
})
const entries = computed(() => createItems(notificationState))
+ const hasActiveQallSession = computed(
+ () =>
+ primaryView.value.type === 'channel' &&
+ getQallingState(primaryView.value.channelId) === 'subView'
+ )
const ephemeralEntries = computed(() =>
[
hasActiveQallSession.value ? ephemeralItems.qallController : undefined,
diff --git a/src/components/Main/StampPicker/StampPickerContainer.vue b/src/components/Main/StampPicker/StampPickerContainer.vue
index 00ad6cecb7..ccb2c0f867 100644
--- a/src/components/Main/StampPicker/StampPickerContainer.vue
+++ b/src/components/Main/StampPicker/StampPickerContainer.vue
@@ -35,6 +35,14 @@ const style = computed(() => {
transform: 'translateX(-100%)'
}
}
+ if (alignment.value === 'bottom-left') {
+ return {
+ bottom: `min(calc(100% - ${height + margin}px), calc(100% - ${
+ position.value.y
+ }px))`,
+ left: `min(calc(100% - ${width}px), ${position.value.x}px)`
+ }
+ }
if (alignment.value === 'bottom-right') {
return {
bottom: `min(calc(100% - ${height + margin}px), calc(100% - ${
diff --git a/src/components/Settings/composables/useNavigation.ts b/src/components/Settings/composables/useNavigation.ts
index 3957e63857..0849afd4ea 100644
--- a/src/components/Settings/composables/useNavigation.ts
+++ b/src/components/Settings/composables/useNavigation.ts
@@ -12,6 +12,7 @@ export type NavigationItemType =
| 'qall'
| 'stamp'
| 'theme'
+ | 'audio'
// TODO: 言語系リソースの置き場所
export const navigationRouteNameTitleMap: Record = {
@@ -20,7 +21,8 @@ export const navigationRouteNameTitleMap: Record = {
settingsBrowser: 'ブラウザ',
settingsQall: '通話 (Qall)',
settingsStamp: 'スタンプ',
- settingsTheme: 'テーマ'
+ settingsTheme: 'テーマ',
+ settingsAudio: '音声'
}
export const navigations: {
@@ -60,6 +62,11 @@ export const navigations: {
routeName: 'settingsTheme',
iconName: 'brightness-6',
iconMdi: true
+ },
+ {
+ routeName: 'settingsAudio',
+ iconName: 'volume-high',
+ iconMdi: true
}
]
diff --git a/src/components/UI/AIcon.vue b/src/components/UI/AIcon.vue
index 444cb74802..b98a4dc25a 100644
--- a/src/components/UI/AIcon.vue
+++ b/src/components/UI/AIcon.vue
@@ -3,7 +3,7 @@
v-if="mdi"
:width="size"
:height="size"
- viewBox="0 0 24 24"
+ :viewBox="`0 0 24 24`"
v-bind="$attrs"
role="img"
:class="$style.icon"
@@ -15,7 +15,7 @@
v-else
:width="size"
:height="size"
- viewBox="0 0 24 24"
+ :viewBox="`0 0 ${size} ${size}`"
v-bind="$attrs"
role="img"
:class="$style.icon"
diff --git a/src/components/UI/InlineMarkdown.vue b/src/components/UI/InlineMarkdown.vue
index 5c189a2322..8668b8272a 100644
--- a/src/components/UI/InlineMarkdown.vue
+++ b/src/components/UI/InlineMarkdown.vue
@@ -11,7 +11,7 @@
+
+
diff --git a/src/views/Settings/QallTab.vue b/src/views/Settings/QallTab.vue
index 0311dfd10e..d813884a5a 100644
--- a/src/views/Settings/QallTab.vue
+++ b/src/views/Settings/QallTab.vue
@@ -68,6 +68,17 @@
デバイスが取得できませんでした。
+
マスターボリューム
const formatNoiseGateThreshold = (v: number) => `${v}dB`
-const { fetchFailed, audioInputDevices } = useDevicesInfo()
+const { fetchFailed, audioInputDevices, audioOutputDevices } = useDevicesInfo()
const audioInputDeviceOptions = computed(() =>
audioInputDevices.value.map(d => ({
@@ -208,6 +219,13 @@ const audioInputDeviceOptions = computed(() =>
}))
)
+const audioOutputDeviceOptions = computed(() =>
+ audioOutputDevices.value.map(d => ({
+ key: d.label,
+ value: d.deviceId
+ }))
+)
+
const voiceOptions = useVoices()
diff --git a/src/views/SettingsPage.vue b/src/views/SettingsPage.vue
index 1a59b959e4..9224b9f553 100644
--- a/src/views/SettingsPage.vue
+++ b/src/views/SettingsPage.vue
@@ -6,6 +6,7 @@
+
@@ -18,6 +19,7 @@ import { RouteName } from '/@/router'
import { defaultSettingsName } from '/@/router/settings'
import { useResponsiveStore } from '/@/store/ui/responsive'
import useLoginCheck from './composables/useLoginCheck'
+import StampPickerContainer from '../components/Main/StampPicker/StampPickerContainer.vue'
const useSettingsRootPathWatcher = (
isMobile: Ref,
diff --git a/src/views/composables/useInitialFetch.ts b/src/views/composables/useInitialFetch.ts
index 48f8f851f2..7749cfc946 100644
--- a/src/views/composables/useInitialFetch.ts
+++ b/src/views/composables/useInitialFetch.ts
@@ -1,6 +1,5 @@
import { ref } from 'vue'
import useLoginCheck from './useLoginCheck'
-import { useDomainRtcStore } from '/@/store/domain/rtc'
import { useUsersStore } from '/@/store/entities/users'
import { useGroupsStore } from '/@/store/entities/groups'
import { useChannelsStore } from '/@/store/entities/channels'
@@ -12,6 +11,8 @@ import { useStaredChannels } from '/@/store/domain/staredChannels'
import { useViewStatesStore } from '/@/store/domain/viewStates'
import { useSubscriptionStore } from '/@/store/domain/subscription'
+// TODO: Qall
+
const useInitialFetch_ = () => {
const { fetchUsers } = useUsersStore()
const { fetchUserGroups } = useGroupsStore()
@@ -23,7 +24,6 @@ const useInitialFetch_ = () => {
const { fetchViewStates } = useViewStatesStore()
const { fetchStampHistory } = useStampHistory()
const { fetchStaredChannels } = useStaredChannels()
- const { fetchRTCState } = useDomainRtcStore()
return () => {
// 初回fetch
fetchUsers()
@@ -40,7 +40,6 @@ const useInitialFetch_ = () => {
fetchStaredChannels()
fetchClipFolders()
- fetchRTCState()
}
}