From 94428ed17054efb56f264fe33cf56d98b03866e7 Mon Sep 17 00:00:00 2001 From: Myx Date: Mon, 18 May 2026 23:14:16 +0200 Subject: [PATCH] fix: Mobile style fixes and other small ui fixes --- .../docs/plugin-development/capabilities.md | 6 +- server/data/metoyou.sqlite | Bin 299008 -> 315392 bytes toju-app/src/app/app.html | 10 ++ toju-app/src/app/app.ts | 2 + .../services/direct-call.service.ts | 38 +++++- .../feature/dm-chat/dm-chat.component.html | 44 +++++-- .../feature/dm-chat/dm-chat.component.ts | 18 ++- .../feature/dm-rail/dm-rail.component.html | 6 +- .../dm-conversation-item.component.html | 2 +- .../dm-conversation-item.component.ts | 5 +- .../dm-conversations-panel.component.html | 12 +- .../dm-conversations-panel.component.ts | 4 +- .../dm-workspace/dm-workspace.component.html | 22 +++- .../dm-workspace/dm-workspace.component.ts | 20 ++- .../services/plugin-client-api.service.ts | 98 +++++++++++++++ .../domain/models/plugin-api.models.ts | 11 ++ .../plugin-manager.component.html | 106 ++++++++-------- .../private-call-controls.component.html | 2 +- ...ivate-call-participant-card.component.html | 22 +--- ...private-call-participant-card.component.ts | 4 +- .../direct-call/private-call.component.html | 84 +++++++++---- .../direct-call/private-call.component.ts | 119 ++++++++++++------ .../room/chat-room/chat-room.component.html | 16 ++- .../room/chat-room/chat-room.component.ts | 22 +++- .../rooms-side-panel.component.ts | 3 + ...voice-workspace-stream-tile.component.html | 104 ++++++++++++++- .../voice-workspace-stream-tile.component.ts | 84 +++++++++++-- .../servers-rail/servers-rail.component.html | 16 ++- .../servers-rail/servers-rail.component.ts | 63 +++++++--- .../profile-card-mobile.component.html | 8 +- .../user-avatar/user-avatar.component.ts | 88 +++++++++---- toju-app/src/app/store/rooms/rooms.reducer.ts | 8 +- 32 files changed, 808 insertions(+), 239 deletions(-) diff --git a/docs-site/docs/plugin-development/capabilities.md b/docs-site/docs/plugin-development/capabilities.md index 85fec4a..65bf99c 100644 --- a/docs-site/docs/plugin-development/capabilities.md +++ b/docs-site/docs/plugin-development/capabilities.md @@ -19,11 +19,11 @@ Capabilities protect privileged app surfaces. A plugin must declare a capability | `messages.editOwn` | `messages.edit()` | Edits plugin-owned messages. | | `messages.deleteOwn` | `messages.delete()` | Deletes plugin-owned messages. | | `messages.moderate` | `messages.moderateDelete()` | Moderation delete path. | -| `messages.sync` | `messages.sync()` | Syncs message arrays into client state. | +| `messages.sync` | `messages.sync()`, `messages.import()`, `attachments.import()` | Syncs message arrays, imports historical messages locally, or imports files for those messages. | | `channels.read` | `channels.list()`, `channels.select()` | Reads and selects channels. | -| `channels.manage` | `channels.addAudioChannel()`, `channels.addVideoChannel()`, `channels.remove()`, `channels.rename()` | Mutates channel or channel-section state. | +| `channels.manage` | `channels.addTextChannel()`, `channels.addAudioChannel()`, `channels.addVideoChannel()`, `channels.remove()`, `channels.rename()` | Mutates channel or channel-section state. | | `server.read` | `server.getCurrent()` | Reads active server. | -| `server.manage` | `server.updatePermissions()`, `server.updateSettings()` | Updates server permissions or settings. | +| `server.manage` | `server.updateIcon()`, `server.updatePermissions()`, `server.updateSettings()` | Updates server icon, permissions, or settings. `server.updateIcon()` resolves when the local icon update has been persisted or rejects if the current user is not allowed to manage the server icon. | | `p2p.data` | `p2p.connectedPeers()`, `p2p.broadcastData()`, `p2p.sendData()` | Uses plugin peer data paths. | | `p2p.media` | Reserved peer media features. | Included for media-facing plugins. | | `media.playAudio` | `media.playAudioClip()` | Plays an audio URL locally. | diff --git a/server/data/metoyou.sqlite b/server/data/metoyou.sqlite index ea76557d6130514253ece9d5fa8d18caa17da11e..19bc27d72137f49c88918e1ea01789a231782ea0 100644 GIT binary patch delta 14253 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 delta 3152 zcma)8eN>cH8h`J-?>q13y?1a##m_-;#SfTy2Sk`f)Kg|!nQLNYrozl16f)DypjH~- zW>B{0pqST6TT^SRW2{N~S{b>eW}PyRX{NR3uv?Z2lBA|BhoxFE=UHab4e##|vu%02mX*{m4l)o}&sv%>fK9}S*B->ZpSSx2>Qr|Q#~Z;E&3k)KD~yGIt)@3tx)r*hty7M?qNV| z4Q_iSVx1>|3piQ&Nsz(W5Al^%a1v~h45#)l!Psg%Zp<`BvQbUIZOv^0QxtAB2VT#J zr8a|h(Js<6u@FtA?er4;iJq}~Z%5qmGuOZX1JLFeHjy?f^LN_eJCqG*6caKXXf*Jb zM0~-;fpA$cT2jPyCfLz1XnMTa&cNb8Su_Ij;^n3777Rf@uGd?=?Qo>V>#OR<0h;>5 z6sbY1>~Osr0!|*!bv&M$4xm66l4E4NMJnMFK-zfmRCUB!y;=+sEk>%7Jb~0&MlDnc zCmel)u;?YYHo+E|y=kYgVcmAvA!elx+q3H}_&6sy;au6wDtEwTV%DTjZMzy^hZciB zS=ZR5_s+TxxQI&*V7^BZVz#|7lPB{wWL16?XSh6a%A4!6{*5y{HwN*eW z90BzLZKL1PeRL-{fHK9;=__a}T7xQ48Jdfx(dX!6)JN~9Q|SbnP6ts;E{oOT!(svX zQM{LYMcyad$VT#K(T=X6f1p=$ zmth!CZ_`ieU+DYwxAl$Udc96xp+Bq_=ri;j-K7uKW$migq8-!rX$_zWZq_zytF*AT zK%1>i*3z|s8qcQM3Ik%r8L?eGuI^VG)ED7Ba1vauKCLcM=d1bZEozRMswSv5rCn)L znv}1U{Yt&EQF#jWQ_7Wj$}Hs;B?r2dRHdI{lYf!FmygOH$~)vua;^M?yhxrckC#Wu zPMO-S*v{F$u^qNG+BVysu`RVNuuZpR*-~st;(Km!vX~(z+lbU5HA_dOPtj261Lq-`M>{Lw%uX zdd+OGimh%&{gYLc7!$a`Q{6k6PT#^Mh0buGG*A!;fR#*ZLHV9;wJfkK0S%8er^nON zosTA>wD{p~+G#O6-ik{5#P{Q#<$&VYuiNV}$AN`RJ%=*blyhh(vt315T8ure^Z>hh z4hY?v+vrFhb`UvQuaPt3QR_W9xrF_pr1d(SGJQjm|RV460(Uo}pk&~_ev)NuehYeoRh&b9J4M4d zay`hy*~nH@&+b;>5Vqkbl-9S)(%l{L(pbgrgRcp!<~&Ma_2<#r3erx#BS*-)N;-L+ ztR~gU60(@gQxeHtWGqP`25!gS;UoBCyc55wypI{KR(9Zs@`|z^`|vD08E4{TjE&32 zF{9DgZoFWu5=R(Ou@`DbE$D>d62CK49*uQ#(DGhH z-ylyvlmWB6W^s44oW&&zi$mcOFfj&X{vTihn{oxYwOHoH^199tSk)EOm)+4Q_D_om zedVEuUpT9yB-;7vj}$Bnq(_Q#*aig-W{r*F>x^MOn`8aM4`iAZY%hj&R?0Qhz3>k zuhQO%-d~_|$w4v_uQb|>nR>n6pXUJT?76YxbJl=AinjvmLas<0y843vMZX~WwM4FQ zpJ+WXM_eH}cpbU&GX^D94a=9Ls;O0Xtr-$~9}RS@Dpu?_09=)@Ca3G+#Ajq(_N1kq z;H517Eh;GucwH{nqOz##j*>DzPttt;X3BJh1EFBV6%9UAo)#)CUszI>77CV@vgEH} z(um)yyx<5N!(Qwqz6{1$;~nB0Acc6KBG#tf;zckj8Vr^e7KA7Hq9wty$WZSCLw(^u zK{OEGXy}-p)so@>d$W%?FaBhFQO&jF1>%2b1$Vq%%oXTGdYVu8!+gf?rrYQyx|Y_^ zWi&+n^gengok%ljDji7eR3ukO3uz)pd9?SC?PM!iPo5=}Bua|OTrz{)O0vlq;v~I^ zf;;ee{5}2}e}?zsxAALu1FpqS;-$D0FW{YaJI=-Kfj9;C!v+?Ni^gf=TjQ{?&)Cf; x`zB+pQDZ3~u@LYy + @if (isMobile() && directCalls.mobileOverlaySession(); as call) { +
+ +
+ } + @if (isThemeStudioFullscreen()) {
([]); + private readonly mobileOverlayCallId = signal(null); readonly sessions = computed(() => this.sessionsSignal()); readonly activeSessions = computed(() => this.sessions().filter((session) => session.status !== 'ended')); @@ -65,6 +68,15 @@ export class DirectCallService { }); readonly currentSession = signal(null); readonly hasActiveCall = computed(() => this.visibleActiveSessions().length > 0); + readonly mobileOverlaySession = computed(() => { + const callId = this.mobileOverlayCallId(); + + if (!callId) { + return null; + } + + return this.visibleActiveSessions().find((session) => session.callId === callId) ?? null; + }); constructor() { this.delivery.directCallEvents$.subscribe((event) => { @@ -92,6 +104,12 @@ export class DirectCallService { this.audio.stop(AppSound.Call); }); + + effect(() => { + if (this.mobileOverlayCallId() && !this.mobileOverlaySession()) { + this.mobileOverlayCallId.set(null); + } + }); } sessionById(callId: string | null | undefined): DirectCallSession | null { @@ -155,7 +173,7 @@ export class DirectCallService { this.currentSession.set(session); await this.joinCall(session.callId, false); this.sendCallEvent(peerParticipant.userId, 'ring', session); - await this.router.navigate(['/call', session.callId]); + await this.openCallView(session.callId); return session; } @@ -186,6 +204,24 @@ export class DirectCallService { this.currentSession.set(session); } + async openCallView(callId: string): Promise { + if (this.viewport.isMobile()) { + await this.openMobileCallOverlay(callId); + return; + } + + await this.openCallView(callId); + } + + async openMobileCallOverlay(callId: string): Promise { + await this.openCall(callId); + this.mobileOverlayCallId.set(callId); + } + + closeMobileCallOverlay(): void { + this.mobileOverlayCallId.set(null); + } + async answerIncomingCall(callId: string): Promise { const session = this.sessionById(callId); diff --git a/toju-app/src/app/domains/direct-message/feature/dm-chat/dm-chat.component.html b/toju-app/src/app/domains/direct-message/feature/dm-chat/dm-chat.component.html index 3fbfd17..20be99e 100644 --- a/toju-app/src/app/domains/direct-message/feature/dm-chat/dm-chat.component.html +++ b/toju-app/src/app/domains/direct-message/feature/dm-chat/dm-chat.component.html @@ -6,17 +6,39 @@ appThemeNode="dmChatHeader" class="flex h-14 shrink-0 items-center gap-3 border-b border-border px-4" > - -
-

{{ peerName() }}

-

{{ isGroupConversation() ? 'Group Chat' : 'Direct Message' }}

-
+ @if (peerUser()) { + + } @else { + +
+

{{ peerName() }}

+

{{ isGroupConversation() ? 'Group Chat' : 'Direct Message' }}

+
+ } @if (showCallButton() && conversation()) {
diff --git a/toju-app/src/app/domains/direct-message/feature/dm-workspace/dm-conversations-panel.component.ts b/toju-app/src/app/domains/direct-message/feature/dm-workspace/dm-conversations-panel.component.ts index 6daee4c..1ca6310 100644 --- a/toju-app/src/app/domains/direct-message/feature/dm-workspace/dm-conversations-panel.component.ts +++ b/toju-app/src/app/domains/direct-message/feature/dm-workspace/dm-conversations-panel.component.ts @@ -2,7 +2,8 @@ import { Component, computed, - inject + inject, + output } from '@angular/core'; import { CommonModule } from '@angular/common'; import { NgIcon, provideIcons } from '@ng-icons/core'; @@ -31,6 +32,7 @@ export class DmConversationsPanelComponent { private readonly theme = inject(ThemeService); readonly directMessages = inject(DirectMessageService); readonly listPanelStyles = computed(() => this.theme.getLayoutItemStyles('dmConversationsPanel')); + readonly conversationSelected = output(); trackConversationId(index: number, conversation: DirectMessageConversation): string { return conversation.id; diff --git a/toju-app/src/app/domains/direct-message/feature/dm-workspace/dm-workspace.component.html b/toju-app/src/app/domains/direct-message/feature/dm-workspace/dm-workspace.component.html index 6521cf0..020bc8a 100644 --- a/toju-app/src/app/domains/direct-message/feature/dm-workspace/dm-workspace.component.html +++ b/toju-app/src/app/domains/direct-message/feature/dm-workspace/dm-workspace.component.html @@ -13,7 +13,10 @@
- +
@@ -32,7 +35,21 @@ class="h-5 w-5" /> -

Direct messages

+

Direct messages

+ @if (activeCall()) { + + }
@@ -50,4 +67,3 @@
} - diff --git a/toju-app/src/app/domains/direct-message/feature/dm-workspace/dm-workspace.component.ts b/toju-app/src/app/domains/direct-message/feature/dm-workspace/dm-workspace.component.ts index b982aa6..dd08d02 100644 --- a/toju-app/src/app/domains/direct-message/feature/dm-workspace/dm-workspace.component.ts +++ b/toju-app/src/app/domains/direct-message/feature/dm-workspace/dm-workspace.component.ts @@ -16,10 +16,11 @@ import { ActivatedRoute, Router } from '@angular/router'; import { toSignal } from '@angular/core/rxjs-interop'; import { map } from 'rxjs'; import { NgIcon, provideIcons } from '@ng-icons/core'; -import { lucideChevronLeft } from '@ng-icons/lucide'; +import { lucideChevronLeft, lucidePhoneCall } from '@ng-icons/lucide'; import { ServersRailComponent } from '../../../../features/servers/servers-rail/servers-rail.component'; import { ViewportService } from '../../../../core/platform'; import { ThemeService } from '../../../theme'; +import { DirectCallService } from '../../../direct-call'; import { DirectMessageService } from '../../application/services/direct-message.service'; import { DmChatPanelComponent } from './dm-chat-panel.component'; import { DmConversationsPanelComponent } from './dm-conversations-panel.component'; @@ -47,7 +48,7 @@ interface SwiperElement extends HTMLElement { DmConversationsPanelComponent, ServersRailComponent ], - viewProviders: [provideIcons({ lucideChevronLeft })], + viewProviders: [provideIcons({ lucideChevronLeft, lucidePhoneCall })], schemas: [CUSTOM_ELEMENTS_SCHEMA], templateUrl: './dm-workspace.component.html' }) @@ -57,6 +58,7 @@ export class DmWorkspaceComponent implements OnDestroy { private readonly theme = inject(ThemeService); private readonly viewport = inject(ViewportService); private readonly zone = inject(NgZone); + private readonly directCalls = inject(DirectCallService); private lastSeenConversationId: string | null = null; private swiperListenerAttached: SwiperElement | null = null; readonly directMessages = inject(DirectMessageService); @@ -66,6 +68,12 @@ export class DmWorkspaceComponent implements OnDestroy { readonly layoutStyles = computed(() => this.theme.getLayoutContainerStyles('dmLayout')); readonly isMobile = this.viewport.isMobile; readonly swiperRef = viewChild>('swiperEl'); + readonly activeCall = computed(() => { + const currentSession = this.directCalls.currentSession(); + const visibleSessions = this.directCalls.visibleActiveSessions(); + + return visibleSessions.find((session) => session.callId === currentSession?.callId) ?? visibleSessions[0] ?? null; + }); /** Active page within the mobile single-pane navigation flow. Ignored on desktop. */ readonly mobilePage = signal('conversations'); @@ -150,6 +158,14 @@ export class DmWorkspaceComponent implements OnDestroy { this.mobilePage.set(page); } + openActiveCall(): void { + const call = this.activeCall(); + + if (call) { + void this.directCalls.openCallView(call.callId); + } + } + ngOnDestroy(): void { this.directMessages.closeConversationView(this.routeConversationId()); } diff --git a/toju-app/src/app/domains/plugins/application/services/plugin-client-api.service.ts b/toju-app/src/app/domains/plugins/application/services/plugin-client-api.service.ts index 3a65d12..1484000 100644 --- a/toju-app/src/app/domains/plugins/application/services/plugin-client-api.service.ts +++ b/toju-app/src/app/domains/plugins/application/services/plugin-client-api.service.ts @@ -3,6 +3,9 @@ import { Store } from '@ngrx/store'; import { Subscription } from 'rxjs'; import { RealtimeSessionFacade } from '../../../../core/realtime'; import { DatabaseService } from '../../../../infrastructure/persistence'; +import { ServerDirectoryFacade } from '../../../server-directory'; +import { resolveRoomPermission } from '../../../access-control'; +import { AttachmentFacade } from '../../../attachment'; import { VoiceConnectionFacade } from '../../../voice-connection/application/facades/voice-connection.facade'; import type { Channel, @@ -28,6 +31,7 @@ import type { PluginApiAvatarUpdate, PluginApiActionContext, PluginApiActionSource, + PluginApiAttachmentImportRequest, PluginApiChannelRequest, PluginApiCustomStreamRequest, PluginApiMessageAsPluginUserRequest, @@ -44,11 +48,13 @@ import { PluginUiRegistryService } from './plugin-ui-registry.service'; @Injectable({ providedIn: 'root' }) export class PluginClientApiService { + private readonly attachments = inject(AttachmentFacade); private readonly capabilities = inject(PluginCapabilityService); private readonly db = inject(DatabaseService); private readonly logger = inject(PluginLoggerService); private readonly messageBus = inject(PluginMessageBusService); private readonly realtime = inject(RealtimeSessionFacade); + private readonly serverDirectory = inject(ServerDirectoryFacade); private readonly store = inject(Store); private readonly storage = inject(PluginStorageService); private readonly uiRegistry = inject(PluginUiRegistryService); @@ -73,6 +79,10 @@ export class PluginClientApiService { requireCapability('channels.manage'); this.store.dispatch(RoomsActions.addChannel({ channel: createChannel(request, 'voice') })); }, + addTextChannel: (request) => { + requireCapability('channels.manage'); + this.store.dispatch(RoomsActions.addChannel({ channel: createChannel(request, 'text') })); + }, addVideoChannel: (request) => { requireCapability('channels.manage'); this.uiRegistry.registerChannelSection(pluginId, request.id ?? request.name, { @@ -143,6 +153,15 @@ export class PluginClientApiService { await this.storage.writeClientData(pluginId, key, value); } }, + attachments: { + import: async (request: PluginApiAttachmentImportRequest) => { + requireCapability('messages.sync'); + const roomId = this.requireRoomId(); + + this.attachments.rememberMessageRoom(request.messageId, roomId); + await this.attachments.publishAttachments(request.messageId, request.files, this.currentUser()?.id); + } + }, media: { addCustomAudioStream: async (request) => { requireCapability('media.addAudioStream'); @@ -190,6 +209,10 @@ export class PluginClientApiService { requireCapability('messages.send'); this.receivePluginUserMessage(pluginId, request); }, + import: async (messages) => { + requireCapability('messages.sync'); + await this.importPluginMessages(pluginId, messages); + }, setTyping: (isTyping, channelId) => { requireCapability('messages.send'); this.setTyping(pluginId, isTyping, channelId); @@ -301,6 +324,58 @@ export class PluginClientApiService { return userId; }, + updateIcon: async (icon) => { + requireCapability('server.manage'); + const room = this.currentRoom(); + const currentUser = this.currentUser(); + + if (!room) { + throw new Error('Room not found'); + } + + if (!currentUser) { + throw new Error('Not logged in'); + } + + const isOwner = room.hostId === currentUser.id || room.hostId === currentUser.oderId; + const isServerAdmin = currentUser.role === 'admin' || currentUser.role === 'host'; + const canByRole = resolveRoomPermission(room, currentUser, 'manageIcon'); + + if (!isOwner && !isServerAdmin && !canByRole) { + throw new Error('Permission denied'); + } + + const iconUpdatedAt = Date.now(); + + await this.db.updateRoom(room.id, { icon, iconUpdatedAt }); + + this.store.dispatch(RoomsActions.updateServerIconSuccess({ roomId: room.id, icon, iconUpdatedAt })); + + this.realtime.broadcastMessage({ + type: 'server-icon-update', + roomId: room.id, + icon, + iconUpdatedAt + }); + + this.realtime.sendRawMessage({ + type: 'server_icon_available', + serverId: room.id, + iconUpdatedAt + }); + + this.serverDirectory.updateServer(room.id, { + actingRole: isOwner ? 'host' : undefined, + currentOwnerId: currentUser.id, + icon, + iconUpdatedAt + }, { + sourceId: room.sourceId, + sourceUrl: room.sourceUrl + }).subscribe({ + error: () => {} + }); + }, updatePermissions: (permissions) => { requireCapability('server.manage'); this.store.dispatch(RoomsActions.updateRoomPermissions({ roomId: this.requireRoomId(), permissions })); @@ -648,6 +723,29 @@ export class PluginClientApiService { }); } + private async importPluginMessages(pluginId: string, messages: Message[]): Promise { + const roomId = this.requireRoomId(); + const normalizedMessages = messages + .filter((message) => message.roomId === roomId) + .map((message) => ({ + ...message, + channelId: message.channelId ?? this.activeChannelId() ?? 'general', + isDeleted: message.isDeleted === true, + reactions: message.reactions ?? [] + })); + + if (normalizedMessages.length === 0) { + return; + } + + for (const message of normalizedMessages) { + await this.db.saveMessage(message); + } + + this.store.dispatch(MessagesActions.syncMessages({ messages: normalizedMessages })); + this.logger.info(pluginId, 'Historical messages imported', { count: normalizedMessages.length }); + } + private persistPluginMessageUpdate(pluginId: string, messageId: string, updates: Partial): void { void this.db.updateMessage(messageId, updates).catch((error: unknown) => { this.logger.warn(pluginId, 'Failed to persist plugin message update', error); diff --git a/toju-app/src/app/domains/plugins/domain/models/plugin-api.models.ts b/toju-app/src/app/domains/plugins/domain/models/plugin-api.models.ts index 3782cc0..1f8d8de 100644 --- a/toju-app/src/app/domains/plugins/domain/models/plugin-api.models.ts +++ b/toju-app/src/app/domains/plugins/domain/models/plugin-api.models.ts @@ -74,6 +74,11 @@ export interface PluginApiAudioClipRequest { url: string; } +export interface PluginApiAttachmentImportRequest { + files: File[]; + messageId: string; +} + export interface PluginApiCustomStreamRequest { label?: string; stream: MediaStream; @@ -195,6 +200,7 @@ export interface PluginApiUiContributionMap { export interface TojuClientPluginApi { readonly channels: { addAudioChannel: (request: PluginApiChannelRequest) => void; + addTextChannel: (request: PluginApiChannelRequest) => void; addVideoChannel: (request: PluginApiChannelRequest) => void; list: () => Channel[]; remove: (channelId: string) => void; @@ -221,6 +227,9 @@ export interface TojuClientPluginApi { remove: (key: string) => Promise; write: (key: string, value: unknown) => Promise; }; + readonly attachments: { + import: (request: PluginApiAttachmentImportRequest) => Promise; + }; readonly media: { addCustomAudioStream: (request: PluginApiCustomStreamRequest) => Promise; addCustomVideoStream: (request: PluginApiCustomStreamRequest) => Promise; @@ -235,6 +244,7 @@ export interface TojuClientPluginApi { readCurrent: () => Message[]; send: (content: string, channelId?: string) => Message; sendAsPluginUser: (request: PluginApiMessageAsPluginUserRequest) => void; + import: (messages: Message[]) => Promise; setTyping: (isTyping: boolean, channelId?: string) => void; subscribeTyping: (handler: (event: PluginApiTypingEvent) => void) => TojuPluginDisposable; sync: (messages: Message[]) => void; @@ -261,6 +271,7 @@ export interface TojuClientPluginApi { readonly server: { getCurrent: () => Room | null; registerPluginUser: (request: PluginApiPluginUserRequest) => string; + updateIcon: (icon: string) => Promise; updatePermissions: (permissions: Partial) => void; updateSettings: (settings: PluginApiServerSettingsUpdate) => void; }; diff --git a/toju-app/src/app/domains/plugins/feature/plugin-manager/plugin-manager.component.html b/toju-app/src/app/domains/plugins/feature/plugin-manager/plugin-manager.component.html index 3812e33..193d701 100644 --- a/toju-app/src/app/domains/plugins/feature/plugin-manager/plugin-manager.component.html +++ b/toju-app/src/app/domains/plugins/feature/plugin-manager/plugin-manager.component.html @@ -1,13 +1,13 @@
-
-
+
+
- - +
+ + +
-
+
@switch (activeTab()) { @case ('extensions') {
@@ -216,7 +218,7 @@ @for (entry of entries(); track trackEntry($index, entry)) { }
-
+
@if (selectedPlugin(); as plugin) {

{{ plugin.manifest.title }} settings

@if (selectedSettingsPages().length > 0) { @@ -255,7 +257,7 @@ @for (entry of entries(); track trackEntry($index, entry)) { }
-
+
@if (selectedPlugin(); as plugin) {

{{ plugin.manifest.title }}

{{ plugin.manifest.description }}

@for (doc of selectedDocs(); track doc.label) { @@ -323,7 +325,7 @@
@if (entries().length === 0) {
@@ -351,17 +353,17 @@

{{ entry.manifest.description }}

{{ entry.manifest.id }}

-
+
-