diff --git a/package-lock.json b/package-lock.json index d501471f..4e417099 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MPL-2.0", "dependencies": { "@prose-im/prose-core-views": "0.25.0", - "@prose-im/prose-sdk-js": "0.1.27", + "@prose-im/prose-sdk-js": "0.1.28", "browser-downloads": "0.2.x", "browser-image-resizer": "2.4.x", "crisp-countries-languages": "0.8.x", @@ -308,9 +308,9 @@ } }, "node_modules/@prose-im/prose-sdk-js": { - "version": "0.1.27", - "resolved": "https://registry.npmjs.org/@prose-im/prose-sdk-js/-/prose-sdk-js-0.1.27.tgz", - "integrity": "sha512-JsfTwMGGyZsdYmK1HYkKouHCzfSX0AFTVlihcTjWg3+2NHIvQBc6qsQisYA6+/5J1RxtWxFaiUEGPdjVREI3pQ==" + "version": "0.1.28", + "resolved": "https://registry.npmjs.org/@prose-im/prose-sdk-js/-/prose-sdk-js-0.1.28.tgz", + "integrity": "sha512-X+S226AM1zEGf+uxYDk/ktc+WsVnEOE9XCA/yUBZrpCHDT5EaQU/gdEeIXnTUNdpR2JUn2RLxwBDyYqaosGnNg==" }, "node_modules/@trysound/sax": { "version": "0.2.0", diff --git a/package.json b/package.json index ba8eca70..a7af0d6d 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "test": "npm run lint" }, "dependencies": { - "@prose-im/prose-sdk-js": "0.1.27", + "@prose-im/prose-sdk-js": "0.1.28", "@prose-im/prose-core-views": "0.25.0", "lodash.camelcase": "4.3.x", "lodash.upperfirst": "4.3.x", diff --git a/src/assemblies/app/AppSidebar.vue b/src/assemblies/app/AppSidebar.vue index 7888d201..6e4c15ac 100644 --- a/src/assemblies/app/AppSidebar.vue +++ b/src/assemblies/app/AppSidebar.vue @@ -108,13 +108,16 @@ export default { async addContactGroup(jidString: string): Promise { const jids = jidString.split(",").map(value => new JID(value.trim())); - // Add contact - await Broker.$room.createGroup(jids); + await Broker.$room.startConversation(jids); - BaseAlert.success("Group added", "Group has been added"); + // More than one JID involved? A group was created. + if (jids.length > 1) { + BaseAlert.success("Group added", "Group has been added"); + } }, async addContactChannel(jidString: string): Promise { + // TODO: also support creating a private channel w/ createPrivateChannel? await Broker.$room.createPublicChannel(jidString); BaseAlert.success("Channel added", "Channel has been added"); diff --git a/src/assemblies/inbox/InboxTopbar.vue b/src/assemblies/inbox/InboxTopbar.vue index c629ce3c..f8edc4a4 100644 --- a/src/assemblies/inbox/InboxTopbar.vue +++ b/src/assemblies/inbox/InboxTopbar.vue @@ -82,6 +82,20 @@ layout-toolbar( span.a-inbox-topbar__identity-value.u-bold | {{ room.name }} + base-tooltip( + align="right" + direction="bottom" + tooltip="Toggle favorite" + class="a-inbox-topbar__identity-action" + ) + base-icon( + v-if="identityFavoriteIcon" + @click="onIdentityActionFavoriteClick" + :name="identityFavoriteIcon" + size="14px" + class="a-inbox-topbar__identity-action-icon" + ) + template( v-slot:right ) @@ -171,6 +185,7 @@ import { JID, Room, RoomID, RoomType } from "@prose-im/prose-sdk-js"; import Store from "@/store"; // PROJECT: COMPONENTS +import BaseAlert from "@/components/base/BaseAlert.vue"; import { Item as PopoverItem, ItemType as PopoverItemType @@ -217,6 +232,14 @@ export default { return Store.$history; }, + profile(): ReturnType { + return Store.$profile.getProfile(this.jid); + }, + + roomItem(): ReturnType { + return this.room ? Store.$room.getRoomItem(this.room.id) : undefined; + }, + originalJID(): string { return this.jid.toString(); }, @@ -235,10 +258,6 @@ export default { return null; }, - profile(): ReturnType { - return Store.$profile.getProfile(this.jid); - }, - identityBadge(): IdentityBadge { // Identity verified? if (this.profile.security && this.profile.security.verification) { @@ -255,6 +274,14 @@ export default { }; }, + identityFavoriteIcon(): string | null { + if (this.roomItem !== undefined) { + return this.roomItem.isFavorite === true ? "star.fill" : "star"; + } + + return null; + }, + hasActionHistoryPrevious(): boolean { return this.history.adjacent.previous.length > 0; }, @@ -298,7 +325,7 @@ export default { const items: Array = []; Array.from(historyRawRoomIDs).forEach((historyRoomID: RoomID) => { - const historyRoom = Store.$room.getRoomByID(historyRoomID) || undefined; + const historyRoom = Store.$room.getRoom(historyRoomID) || undefined; if (historyRoom !== undefined) { items.push({ @@ -395,6 +422,34 @@ export default { onActionHistoryDropdownClick(): void { // Toggle popover this.isActionHistoryPopoverVisible = !this.isActionHistoryPopoverVisible; + }, + + async onIdentityActionFavoriteClick(): Promise { + if (this.roomItem) { + try { + const wasFavorite = this.roomItem.isFavorite || false; + + // Toggle favorite mode + await this.roomItem.toggleFavorite(); + + // Acknowledge toggle + BaseAlert.info( + wasFavorite === true ? "Unset from favorites" : "Set as favorite", + (wasFavorite === true ? "Removed from" : "Added to") + " favorites" + ); + } catch (error) { + // Alert of toggle error + this.$log.error( + `Could not toggle favorite status on room: ${this.roomItem.room.id}`, + error + ); + + BaseAlert.error( + "Cannot toggle favorite", + "Failed changing favorite status. Try again?" + ); + } + } } } }; @@ -431,9 +486,14 @@ $c: ".a-inbox-topbar"; color: rgb(var(--color-text-primary)); font-size: 16px; - #{$c}__identity-badge { + #{$c}__identity-badge, + #{$c}__identity-action { margin-block-start: 1px; } + + #{$c}__identity-action { + margin-inline-start: 8px; + } } &--jid { @@ -455,13 +515,40 @@ $c: ".a-inbox-topbar"; } } - #{$c}__identity-badge { + &:hover { + #{$c}__identity-action { + visibility: visible; + } + } + + #{$c}__identity-badge, + #{$c}__identity-action { flex: 0 0 auto; + } + #{$c}__identity-badge { + #{$c}__identity-value { margin-inline-start: 5px; } } + + #{$c}__identity-action { + visibility: hidden; + + #{$c}__identity-action-icon { + fill: rgb(var(--color-base-blue-normal)); + display: block; + cursor: pointer; + + &:hover { + fill: rgb(var(--color-base-blue-dark)); + } + + &:active { + fill: darken-var(var(--color-base-blue-dark), 5%); + } + } + } } } diff --git a/src/assets/images/icons/star.fill.svg b/src/assets/images/icons/star.fill.svg new file mode 100644 index 00000000..11ad008c --- /dev/null +++ b/src/assets/images/icons/star.fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/icons/star.svg b/src/assets/images/icons/star.svg new file mode 100644 index 00000000..938f1783 --- /dev/null +++ b/src/assets/images/icons/star.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/broker/delegate.ts b/src/broker/delegate.ts index 689c7ca4..39ab411a 100644 --- a/src/broker/delegate.ts +++ b/src/broker/delegate.ts @@ -40,6 +40,8 @@ class BrokerDelegate implements ProseClientDelegate { } clientConnected(): void { + logger.info("Client connected"); + this.__eventBus.emit("client:connected"); } @@ -48,6 +50,8 @@ class BrokerDelegate implements ProseClientDelegate { const message = "message" in error ? error.message : ""; logger.warn(`Client disconnected. Reason: ${error.code}. ${message}`); + } else { + logger.info(`Client disconnected`); } this.__eventBus.emit("client:disconnected"); @@ -63,16 +67,22 @@ class BrokerDelegate implements ProseClientDelegate { Store.$inbox.setComposing(room.id, composingUsers); } - roomsChanged(): void { + sidebarChanged(): void { + logger.info("Sidebar changed"); + Store.$room.markRoomsChanged(); Store.$room.load(); } contactChanged(_client: ProseClient, jid: JID): void { + logger.info(`Contact changed: ${jid}`); + Store.$roster.markContactChanged(jid); } avatarChanged(_client: ProseClient, jid: JID): void { + logger.info(`Avatar changed: ${jid}`); + Store.$avatar.load(jid); } @@ -81,6 +91,12 @@ class BrokerDelegate implements ProseClientDelegate { room: Room, messageIDs: string[] ): Promise { + logger.info( + `Messages appended in room: ${ + room.id + } with identifiers: ${messageIDs.join(", ")}` + ); + const messages = await room.loadMessagesWithIDs(messageIDs); // Insert all appended messages @@ -108,6 +124,12 @@ class BrokerDelegate implements ProseClientDelegate { room: Room, messageIDs: string[] ): void { + logger.info( + `Messages deleted in room: ${room.id} with identifiers: ${messageIDs.join( + ", " + )}` + ); + for (const messageID of messageIDs) { Store.$inbox.retractMessage(room.id, messageID); } @@ -118,6 +140,12 @@ class BrokerDelegate implements ProseClientDelegate { room: Room, messageIDs: string[] ): Promise { + logger.info( + `Messages updated in room: ${room.id} with identifiers: ${messageIDs.join( + ", " + )}` + ); + const messages = await room.loadMessagesWithIDs(messageIDs); for (const message of messages) { diff --git a/src/broker/modules/profile.ts b/src/broker/modules/profile.ts index 5bc7980b..0e16bad6 100644 --- a/src/broker/modules/profile.ts +++ b/src/broker/modules/profile.ts @@ -47,7 +47,7 @@ interface SaveAvatarRequestMetadata { * ************************************************************************* */ class BrokerModuleProfile extends BrokerModule { - async loadUserProfile(jid: JID): Promise { + async loadUserProfile(jid: JID): Promise { // XEP-0292: vCard4 Over XMPP // https://xmpp.org/extensions/xep-0292.html @@ -56,7 +56,7 @@ class BrokerModuleProfile extends BrokerModule { return await this._client.client?.loadUserProfile(jid); } - async loadUserMetadata(jid: JID): Promise { + async loadUserMetadata(jid: JID): Promise { // XEP-0012: Last Activity + XEP-0202: Entity Time // https://xmpp.org/extensions/xep-0012.html + \ // https://xmpp.org/extensions/xep-0202.html diff --git a/src/broker/modules/room.ts b/src/broker/modules/room.ts index 5982e583..c2eeef3f 100644 --- a/src/broker/modules/room.ts +++ b/src/broker/modules/room.ts @@ -9,14 +9,11 @@ * ************************************************************************* */ // NPM -import { Channel, JID, Room } from "@prose-im/prose-sdk-js"; +import { Channel, JID, SidebarItem } from "@prose-im/prose-sdk-js"; // PROJECT: BROKER import BrokerModule from "@/broker/modules"; -// PROJECT: STORES -import Store from "@/store"; - /************************************************************************** * CLASS * ************************************************************************* */ @@ -26,40 +23,26 @@ class BrokerModuleRoom extends BrokerModule { await this._client.client?.startObservingRooms(); } - connectedRooms(): Room[] { - return this._client.client?.connectedRooms() || []; + sidebarItems(): SidebarItem[] { + return this._client.client?.sidebarItems() || []; } async loadPublicChannels(): Promise> { return (await this._client.client?.loadPublicChannels()) || []; } - async createGroup(participants: Array): Promise { - const room = (await this._client.client?.createGroup( + async startConversation(participants: Array): Promise { + await this._client.client?.startConversation( participants.map(jid => jid.toString()) - )) as Room; - - if (room) { - Store.$room.insertRoom(room); - } + ); } async createPublicChannel(name: string): Promise { - const room = (await this._client.client?.createPublicChannel(name)) as Room; - - if (room) { - Store.$room.insertRoom(room); - } + await this._client.client?.createPublicChannel(name); } async createPrivateChannel(name: string): Promise { - const room = (await this._client.client?.createPrivateChannel( - name - )) as Room; - - if (room) { - Store.$room.insertRoom(room); - } + await this._client.client?.createPrivateChannel(name); } } diff --git a/src/broker/modules/status.ts b/src/broker/modules/status.ts index 7bfd6ce0..db63d42c 100644 --- a/src/broker/modules/status.ts +++ b/src/broker/modules/status.ts @@ -36,7 +36,7 @@ class BrokerModuleStatus extends BrokerModule { await this._client.client?.sendActivity(icon, text); if (this._client.jid) { - let activity: UserActivity | undefined; + let activity: UserActivity | void; if (icon && text) { activity = new UserActivity(icon, text); diff --git a/src/components/base/BaseAvatar.vue b/src/components/base/BaseAvatar.vue index 0d0e8f07..ffee7f76 100644 --- a/src/components/base/BaseAvatar.vue +++ b/src/components/base/BaseAvatar.vue @@ -186,7 +186,7 @@ export default { }, roomName(): string { - const room = Store.$room.getRoomByID(this.jid.toString() as RoomID); + const room = Store.$room.getRoom(this.jid.toString() as RoomID); return room?.name || ""; } diff --git a/src/components/inbox/InboxMessaging.vue b/src/components/inbox/InboxMessaging.vue index f0edfefd..902b4419 100644 --- a/src/components/inbox/InboxMessaging.vue +++ b/src/components/inbox/InboxMessaging.vue @@ -431,7 +431,7 @@ export default { // TODO: migrate to client-provided self name (do not source from \ // roster anymore, returns jid-based nickname) name: Store.$roster.getEntryName(this.selfJID), - avatar: Store.$avatar.getAvatarDataUrl(this.selfJID) + avatar: Store.$avatar.getAvatarDataUrl(this.selfJID) || undefined }); }, @@ -443,7 +443,7 @@ export default { // Identify remote party runtime.MessagingStore.identify(jid.toString(), { name, - avatar: Store.$avatar.getAvatarDataUrl(jid) + avatar: Store.$avatar.getAvatarDataUrl(jid) || undefined }); }, diff --git a/src/components/list/ListButton.vue b/src/components/list/ListButton.vue index d76728a9..3f575b83 100644 --- a/src/components/list/ListButton.vue +++ b/src/components/list/ListButton.vue @@ -151,9 +151,19 @@ $c: ".c-list-button"; } #{$c}__details { + line-height: 0; margin-inline-start: $size-list-item-details-margin-inline-start; display: flex; + align-items: center; flex: 0 0 auto; + + > * { + margin-inline-end: 6px; + + &:last-child { + margin-inline-end: 0; + } + } } // --> SIZES <-- diff --git a/src/components/sidebar/SidebarMain.vue b/src/components/sidebar/SidebarMain.vue index 06a2ae51..6abf5dbd 100644 --- a/src/components/sidebar/SidebarMain.vue +++ b/src/components/sidebar/SidebarMain.vue @@ -47,12 +47,40 @@ :list-class="disclosureListClass" title="Favorites" ) - sidebar-main-item-user( - v-for="itemFavorite in itemFavorites" - :jid="itemFavorite.jid" - :name="itemFavorite.name" - :active="selectedJID && itemFavorite.jid.equals(selectedJID)" + template( + v-for="item in itemFavorites" ) + sidebar-main-item-user( + v-if="item.room.type === roomType.DirectMessage" + :jid="item.room.members[0]?.jid" + :name="item.name" + :unread="item.unreadCount" + :error="item.error" + :draft="item.hasDraft" + :active="item.room.id === selectedRoomID" + ) + + sidebar-main-item-channel( + v-else-if="item.room.type === roomType.Group" + :id="item.room.id" + :name="item.name" + :unread="item.unreadCount" + :error="item.error" + :draft="item.hasDraft" + :active="item.room.id === selectedRoomID" + type="group" + ) + + sidebar-main-item-channel( + v-else-if="item.room.type === roomType.PublicChannel || item.room.type === roomType.PrivateChannel" + :id="item.room.id" + :name="item.name" + :unread="item.unreadCount" + :error="item.error" + :draft="item.hasDraft" + :active="item.room.id === selectedRoomID" + type="channel" + ) list-disclosure( @toggle="onGroupsToggle" @@ -62,25 +90,28 @@ expanded ) template( - v-for="room in itemDirectMessages" + v-for="item in itemDirectMessages" ) - template( - v-if="!itemFavoriteRawJIDs.has(room.id)" + sidebar-main-item-user( + v-if="item.room.type === roomType.DirectMessage" + :jid="item.room.members[0]?.jid" + :name="item.name" + :unread="item.unreadCount" + :error="item.error" + :draft="item.hasDraft" + :active="item.room.id === selectedRoomID" + ) + + sidebar-main-item-channel( + v-else-if="item.room.type === roomType.Group" + :id="item.room.id" + :name="item.name" + :unread="item.unreadCount" + :error="item.error" + :draft="item.hasDraft" + :active="item.room.id === selectedRoomID" + type="group" ) - sidebar-main-item-user( - v-if="room.type === roomType.DirectMessage" - :jid="room.members[0]?.jid" - :name="room.name" - :active="room.id === selectedRoomID" - ) - - sidebar-main-item-channel( - v-if="room.type === roomType.Group" - :id="room.id" - :name="room.name" - :active="room.id === selectedRoomID" - type="group" - ) sidebar-main-item-add( @click="onDirectMessageAddClick" @@ -95,10 +126,13 @@ expanded ) sidebar-main-item-channel( - v-for="room in itemChannels" - :id="room.id" - :name="room.name" - :active="room.id === selectedRoomID" + v-for="item in itemChannels" + :id="item.room.id" + :name="item.name" + :unread="item.unreadCount" + :error="item.error" + :draft="item.hasDraft" + :active="item.room.id === selectedRoomID" type="channel" ) @@ -114,19 +148,13 @@