Prompt
Do a technical analysis of the javascript/vue code base. Focus on technical debt topics and outline where similar things use different concepts and should be aligned, use of deprecated and old functionality, code and writing style that steams from translating the app from backbone and vue2 to the newest vue3 nowerdays, and similar things.
Technical Debt Analysis: Nextcloud Talk Frontend (src/)
The codebase is functionally on Vue 3 with no compat-mode remnants, but it carries three distinct
generational layers: Backbone/SimpleWebRTC-era JavaScript (~5,000 lines), Vue-2-style Options API
components (the majority of SFCs), and modern <script setup lang="ts"> + Pinia code. The debt is not
evenly spread — the call/WebRTC layer and the three remaining Vuex stores concentrate most of it. Here
is the breakdown, roughly ordered by impact.
Dual state management: Vuex and Pinia, bidirectionally coupled
This is the biggest architectural debt. Three large Vuex modules hold the core domain (src/store/:
conversationsStore.js 1,377 lines, messagesStore.js 1,485, participantsStore.js 1,306), while 24 Pinia
stores (src/stores/) hold everything else. The coupling goes in both directions:
- Pinia → Vuex: stores/reactions.js:15 imports the Vuex store and calls
store.commit('addReactionToMessage', ...); stores/session.ts reads store.getters.findParticipant(...)
and commits updateParticipant; stores/breakoutRooms.ts dispatches
patchConversations/deleteConversation; stores/chat.ts:97 reads raw Vuex state via
store.state.messagesStore.messages[token].
- Vuex → Pinia: conversationsStore.js imports ten Pinia stores. deleteConversation (around line 404) is
a manual fan-out: purge chatExtras, groupware, reactions, sharedItems stores, then dispatch into
messagesStore, then commit locally — non-atomic, easy to get out of sync when a new store is added.
- Reactions state is duplicated between messagesStore.js (mutations
addReactionToMessage/removeReactionFromMessage) and stores/reactions.js, kept in sync manually.
- Module-level Pinia instantiation in conversationsStore.js:82 and participantsStore.js:47 (const
tokenStore = useTokenStore(pinia) at import time) is an init-order hazard the lazy in-action pattern
used elsewhere avoids.
Within Pinia itself there are two competing styles: 9 setup-style stores (actor.ts, token.ts,
chatExtras.ts, …) vs ~14 options-style stores (bots.ts, session.ts, callView.ts, …), plus 4
still-untyped JS stores (integrations.js, reactions.js, sounds.js, talkHash.js). Stores also
communicate via the EventBus (token.ts and session.ts listen to signaling-join-room; upload.ts emits
four upload-lifecycle events) instead of reactivity — which bypasses Pinia devtools and makes data flow
hard to trace.
Alignment target: migrate the three Vuex modules to Pinia (this would also kill the duplicated
reactions state and most of the cross-coupling), standardize on setup-style stores, and convert the 4
JS stores to TS.
Backbone/SimpleWebRTC-era call layer (~4,800 lines of pre-Vue code)
The naming alone betrays the origin — *Model.js, *Collection.js is straight Backbone vocabulary, even
though the files date from 2019:
- utils/webrtc/simplewebrtc/ (2,009 lines): vendored SimpleWebRTC fork. peer.js (964 lines) is the
oldest code in the repo — util.inherits(Peer, WildEmitter) at line 150, 12 var declarations,
function(){...}.bind(this) promise chains, and deprecated WebRTC APIs: pc.addStream() at peer.js:125
and addstream/removestream event listeners at lines 55–57 (also e2ee/encryption.js:646) — these are
removed from the modern spec and should be addTrack()/ontrack.
- utils/signaling.js (1,617 lines): factory-object + constructor-function architecture
(Signaling.Base/Internal/Standalone), hand-rolled event handling (this.handlers = {} with manual array
splicing), 26 promise chains and zero async/await, untyped.
- utils/webrtc/webrtc.js (1,640 lines): the glue layer, the densest TODO/FIXME cluster in the repo,
including // FIXME: despite all of the above this is a dirty and ugly hack (line ~974).
- utils/webrtc/models/ (1,170 lines): function-constructor "models" using a custom EmitterMixin.js (a
hand-written event emitter, 92 lines) instead of EventTarget. LocalMediaModel.js binds 16 handlers in
its constructor; CallParticipantModel.js binds 10.
- Vue bridges these by hand: 40+ manual .on()/.off() pairs in components (e.g. CallView.vue:535/546
subscribing to callParticipantCollection in mounted/beforeUnmount) — Vue-1-era integration style with
memory-leak potential, where a useModelEvents() composable or reactive wrappers would do.
This drags in three dead dependencies: wildemitter, the util Node polyfill (used only for
util.inherits), and mockconsole (v0.0.1, abandoned). Notably, the newer utils/media/pipeline/ code
(2,262 lines) shows the same domain done right — ES6 classes, no var — so the target style already
exists in-repo.
Options API vs Composition API: two-thirds still Vue-2-style
Verified counts: 57 SFCs use <script setup>, 141 use classic export default {} (of 198 total). Worse
than the split itself is the hybrid pattern — Options API components with a setup() block returning
composables, then data()/computed/methods on top: App.vue:81-244 and ChatView.vue:91-243 are prime
examples. This cascades into paired inconsistencies:
- Props: 112 components with runtime props: {...} vs 46 with typed defineProps()
- Emits: 49 emits: options vs 18 defineEmits()
- ~23 components still reach Vuex via useStore() while 150+ use Pinia hooks
The good news: the worst Vue 2 idioms are gone — zero $set, $on/$off, this.$nextTick, mixins, filters,
mapGetters, ::v-deep, or this.$router. The migration was done carefully; it just stopped at ~30%. Whole
directories are untouched: AdminSettings (15/16 Options API), BreakoutRoomsEditor (4/4), Dashboard
(3/3).
EventBus as a load-bearing architecture (282 call sites)
services/EventBus.ts is at least fully typed (67 event types), but mitt is used for things Vue 3 has
better answers for: UI coordination (focus-message, scroll-chat-to-bottom), store-to-store sync (see
§1), and signaling fan-out (14 emits from signaling.js). Components in Options API must manually pair
EventBus.on/off in mounted/beforeUnmount (e.g. App.vue:246-259). The signaling events are a legitimate
use (bridging the non-Vue layer); the UI-coordination events would be better as provide/inject or store
state. There are also two buses in play: this internal mitt bus and @nextcloud/event-bus
(subscribe/unsubscribe) for cross-app events — usage of the two is occasionally interchangeable in
components.
Deprecated / risky dependencies
Package: crypto-js (8 files: prepareTemporaryMessage.ts, messagesService.ts, stores/session.ts,
participantsStore.js, TurnServer.vue, …)
Where: Only used for SHA hashing
Replacement: Native Web Crypto (crypto.subtle.digest) — async, so needs small refactors
────────────────────────────────────────
Package: hark (unmaintained since ~2016)
Where: SpeakingMonitor.js, useDevices.js
Replacement: Native AudioWorklet/AnalyserNode
────────────────────────────────────────
Package: @matrix-org/olm
Where: e2ee/
Replacement: Deprecated upstream in favor of vodozemac — track for when the e2ee work matures
────────────────────────────────────────
Package: wildemitter, util, mockconsole
Where: simplewebrtc fork only
Replacement: Falls out of §2 refactor for free
────────────────────────────────────────
Package: cropperjs v1 + vue-cropperjs
Where: 1 file
Replacement: cropperjs v2
────────────────────────────────────────
Package: base64-js
Where: e2ee/encryption.js
Replacement: Uint8Array.fromBase64/TextEncoder
────────────────────────────────────────
Package: vue-material-design-icons (421 imports across 150+ files)
Where: everywhere
Replacement: Works, but each icon is a full SFC; @mdi/js + NcIconSvgWrapper is the lighter,
tree-shakeable pattern @nextcloud/vue itself uses
Clean areas worth noting: @nextcloud/vue imports are 100% modern-path (@nextcloud/vue/components/...,
zero dist/ deep imports), no moment.js anywhere (centralized Intl formatters in
utils/formattedTime.ts), Vitest + @vue/test-utils v2 with no jest leftovers, and OC.* global usage is
confined to legitimate integration points (collections.js:11 OC.linkTo() being the one that should move
to @nextcloud/router).
Same thing, different concepts — alignment list
- Dialogs: ~23 files use declarative in templates, ~15 use programmatic spawnDialog()
(including from inside a store: stores/chatExtras.ts:270), and a few still use NcModal — App.vue even
carries a FIXME: Align NcModal header with NcDialog. Pick declarative NcDialog for component-owned
dialogs, spawnDialog only for code without a template context.
- Error handling: three layers handle errors differently — most services are bare axios calls (fine, by
design), but a few catch internally with console.debug (callsService.ts:52-56) or even show UI from a
service (participantsService.js:37-50 calls showWarning()). Stores mostly do try/catch +
showError(t(...)), but some only console.error and revert. Worth writing the rule down: services throw,
stores/composables catch and toast.
- OCS unwrapping: 30+ manual response.data.ocs.data accesses despite typed helpers
(ApiResponseUnwrapped in types/index.ts) existing.
- JS/TS stragglers: 8 of 38 services and 7 of 31 composables are still untyped JS (signalingService.js,
participantsService.js, useDevices.js, useIsInCall.js, …) — mostly the ones touching the legacy call
layer, unsurprisingly. Also 2 legacy default-exports (BrowserStorage.js, SessionStorage.js) vs named
exports everywhere else.
- Loading UI: NcLoadingIcon (36 files) vs legacy icon-loading/icon-loading-small CSS classes (~10
files, e.g. LoadingComponent.vue:6, EditableTextField.vue:66).
- Icons: 121 files on vue-material-design-icons vs 11 still on icon-* CSS classes
(PublicShareAuthSidebar.vue:15).
- Styling: generally excellent (~1,005 var(--color-*) uses vs ~25 hardcoded colors, all brand-ish), but
174 :deep() overrides of @nextcloud/vue internals is a fragility hotspot — each library bump can
silently break these.
Suggested priority order
- Finish the Vuex → Pinia migration (3 modules). Highest leverage: removes the bidirectional coupling,
the duplicated reactions state, the module-level init hazard, and the last useStore() components in
one stroke. Big enough that it warrants a tracking ticket and a per-module split.
- Fix deprecated WebRTC APIs (addStream/addstream in peer.js, encryption.js) — this is a correctness
time-bomb, independent of any rewrite.
- Modernize the call layer incrementally: ES6-classify the models, replace EmitterMixin/wildemitter
with EventTarget, async/await signaling.js, and wrap model subscriptions in a composable — drops three
dead dependencies as a side effect.
- Component migration to <script setup lang="ts">, directory by directory (AdminSettings is a
self-contained start), pulling props/emits typing along with it.
- Convention cleanups (dialogs, error-handling rule, JS service/composable conversion, crypto-js → Web
Crypto, legacy icon classes) — good small/first-timer PRs.
One process note per the repo's AI policy: items 1 and 3 touch multiple subsystems and have real
architectural choices in them — those are exactly the cases where the contribution guidelines ask for a
discussion ticket before implementation, and they'd need to land as a series of focused PRs rather
than one big one.
Prompt
Do a technical analysis of the javascript/vue code base. Focus on technical debt topics and outline where similar things use different concepts and should be aligned, use of deprecated and old functionality, code and writing style that steams from translating the app from backbone and vue2 to the newest vue3 nowerdays, and similar things.
Technical Debt Analysis: Nextcloud Talk Frontend (src/)
The codebase is functionally on Vue 3 with no compat-mode remnants, but it carries three distinct
generational layers: Backbone/SimpleWebRTC-era JavaScript (~5,000 lines), Vue-2-style Options API
components (the majority of SFCs), and modern <script setup lang="ts"> + Pinia code. The debt is not
evenly spread — the call/WebRTC layer and the three remaining Vuex stores concentrate most of it. Here
is the breakdown, roughly ordered by impact.
Dual state management: Vuex and Pinia, bidirectionally coupled
This is the biggest architectural debt. Three large Vuex modules hold the core domain (src/store/:
conversationsStore.js 1,377 lines, messagesStore.js 1,485, participantsStore.js 1,306), while 24 Pinia
stores (src/stores/) hold everything else. The coupling goes in both directions:
store.commit('addReactionToMessage', ...); stores/session.ts reads store.getters.findParticipant(...)
and commits updateParticipant; stores/breakoutRooms.ts dispatches
patchConversations/deleteConversation; stores/chat.ts:97 reads raw Vuex state via
store.state.messagesStore.messages[token].
a manual fan-out: purge chatExtras, groupware, reactions, sharedItems stores, then dispatch into
messagesStore, then commit locally — non-atomic, easy to get out of sync when a new store is added.
addReactionToMessage/removeReactionFromMessage) and stores/reactions.js, kept in sync manually.
tokenStore = useTokenStore(pinia) at import time) is an init-order hazard the lazy in-action pattern
used elsewhere avoids.
Within Pinia itself there are two competing styles: 9 setup-style stores (actor.ts, token.ts,
chatExtras.ts, …) vs ~14 options-style stores (bots.ts, session.ts, callView.ts, …), plus 4
still-untyped JS stores (integrations.js, reactions.js, sounds.js, talkHash.js). Stores also
communicate via the EventBus (token.ts and session.ts listen to signaling-join-room; upload.ts emits
four upload-lifecycle events) instead of reactivity — which bypasses Pinia devtools and makes data flow
hard to trace.
Alignment target: migrate the three Vuex modules to Pinia (this would also kill the duplicated
reactions state and most of the cross-coupling), standardize on setup-style stores, and convert the 4
JS stores to TS.
Backbone/SimpleWebRTC-era call layer (~4,800 lines of pre-Vue code)
The naming alone betrays the origin — *Model.js, *Collection.js is straight Backbone vocabulary, even
though the files date from 2019:
oldest code in the repo — util.inherits(Peer, WildEmitter) at line 150, 12 var declarations,
function(){...}.bind(this) promise chains, and deprecated WebRTC APIs: pc.addStream() at peer.js:125
and addstream/removestream event listeners at lines 55–57 (also e2ee/encryption.js:646) — these are
removed from the modern spec and should be addTrack()/ontrack.
(Signaling.Base/Internal/Standalone), hand-rolled event handling (this.handlers = {} with manual array
splicing), 26 promise chains and zero async/await, untyped.
including // FIXME: despite all of the above this is a dirty and ugly hack (line ~974).
hand-written event emitter, 92 lines) instead of EventTarget. LocalMediaModel.js binds 16 handlers in
its constructor; CallParticipantModel.js binds 10.
subscribing to callParticipantCollection in mounted/beforeUnmount) — Vue-1-era integration style with
memory-leak potential, where a useModelEvents() composable or reactive wrappers would do.
This drags in three dead dependencies: wildemitter, the util Node polyfill (used only for
util.inherits), and mockconsole (v0.0.1, abandoned). Notably, the newer utils/media/pipeline/ code
(2,262 lines) shows the same domain done right — ES6 classes, no var — so the target style already
exists in-repo.
Options API vs Composition API: two-thirds still Vue-2-style
Verified counts: 57 SFCs use <script setup>, 141 use classic export default {} (of 198 total). Worse
than the split itself is the hybrid pattern — Options API components with a setup() block returning
composables, then data()/computed/methods on top: App.vue:81-244 and ChatView.vue:91-243 are prime
examples. This cascades into paired inconsistencies:
The good news: the worst Vue 2 idioms are gone — zero $set, $on/$off, this.$nextTick, mixins, filters,
mapGetters, ::v-deep, or this.$router. The migration was done carefully; it just stopped at ~30%. Whole
directories are untouched: AdminSettings (15/16 Options API), BreakoutRoomsEditor (4/4), Dashboard
(3/3).
EventBus as a load-bearing architecture (282 call sites)
services/EventBus.ts is at least fully typed (67 event types), but mitt is used for things Vue 3 has
better answers for: UI coordination (focus-message, scroll-chat-to-bottom), store-to-store sync (see
§1), and signaling fan-out (14 emits from signaling.js). Components in Options API must manually pair
EventBus.on/off in mounted/beforeUnmount (e.g. App.vue:246-259). The signaling events are a legitimate
use (bridging the non-Vue layer); the UI-coordination events would be better as provide/inject or store
state. There are also two buses in play: this internal mitt bus and @nextcloud/event-bus
(subscribe/unsubscribe) for cross-app events — usage of the two is occasionally interchangeable in
components.
Deprecated / risky dependencies
Package: crypto-js (8 files: prepareTemporaryMessage.ts, messagesService.ts, stores/session.ts,
participantsStore.js, TurnServer.vue, …)
Where: Only used for SHA hashing
Replacement: Native Web Crypto (crypto.subtle.digest) — async, so needs small refactors
────────────────────────────────────────
Package: hark (unmaintained since ~2016)
Where: SpeakingMonitor.js, useDevices.js
Replacement: Native AudioWorklet/AnalyserNode
────────────────────────────────────────
Package: @matrix-org/olm
Where: e2ee/
Replacement: Deprecated upstream in favor of vodozemac — track for when the e2ee work matures
────────────────────────────────────────
Package: wildemitter, util, mockconsole
Where: simplewebrtc fork only
Replacement: Falls out of §2 refactor for free
────────────────────────────────────────
Package: cropperjs v1 + vue-cropperjs
Where: 1 file
Replacement: cropperjs v2
────────────────────────────────────────
Package: base64-js
Where: e2ee/encryption.js
Replacement: Uint8Array.fromBase64/TextEncoder
────────────────────────────────────────
Package: vue-material-design-icons (421 imports across 150+ files)
Where: everywhere
Replacement: Works, but each icon is a full SFC; @mdi/js + NcIconSvgWrapper is the lighter,
tree-shakeable pattern @nextcloud/vue itself uses
Clean areas worth noting: @nextcloud/vue imports are 100% modern-path (@nextcloud/vue/components/...,
zero dist/ deep imports), no moment.js anywhere (centralized Intl formatters in
utils/formattedTime.ts), Vitest + @vue/test-utils v2 with no jest leftovers, and OC.* global usage is
confined to legitimate integration points (collections.js:11 OC.linkTo() being the one that should move
to @nextcloud/router).
Same thing, different concepts — alignment list
(including from inside a store: stores/chatExtras.ts:270), and a few still use NcModal — App.vue even
carries a FIXME: Align NcModal header with NcDialog. Pick declarative NcDialog for component-owned
dialogs, spawnDialog only for code without a template context.
design), but a few catch internally with console.debug (callsService.ts:52-56) or even show UI from a
service (participantsService.js:37-50 calls showWarning()). Stores mostly do try/catch +
showError(t(...)), but some only console.error and revert. Worth writing the rule down: services throw,
stores/composables catch and toast.
(ApiResponseUnwrapped in types/index.ts) existing.
participantsService.js, useDevices.js, useIsInCall.js, …) — mostly the ones touching the legacy call
layer, unsurprisingly. Also 2 legacy default-exports (BrowserStorage.js, SessionStorage.js) vs named
exports everywhere else.
files, e.g. LoadingComponent.vue:6, EditableTextField.vue:66).
(PublicShareAuthSidebar.vue:15).
174 :deep() overrides of @nextcloud/vue internals is a fragility hotspot — each library bump can
silently break these.
Suggested priority order
the duplicated reactions state, the module-level init hazard, and the last useStore() components in
one stroke. Big enough that it warrants a tracking ticket and a per-module split.
time-bomb, independent of any rewrite.
with EventTarget, async/await signaling.js, and wrap model subscriptions in a composable — drops three
dead dependencies as a side effect.
self-contained start), pulling props/emits typing along with it.
Crypto, legacy icon classes) — good small/first-timer PRs.
One process note per the repo's AI policy: items 1 and 3 touch multiple subsystems and have real
architectural choices in them — those are exactly the cases where the contribution guidelines ask for a
discussion ticket before implementation, and they'd need to land as a series of focused PRs rather
than one big one.