Skip to content

Commit

Permalink
feat(groups): build leave group method (#4086)
Browse files Browse the repository at this point in the history
  • Loading branch information
nathan-power committed Jul 29, 2022
1 parent b476faf commit 3d10d27
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 10 deletions.
9 changes: 7 additions & 2 deletions components/views/navigation/sidebar/list/item/Item.vue
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export default Vue.extend({
computed: {
...mapState({
ui: (state) => (state as RootState).ui,
accounts: (state) => (state as RootState).accounts,
}),
...mapGetters('settings', ['getTimestamp', 'getDate']),
contextMenuValues(): ContextMenuItem[] {
Expand All @@ -58,7 +59,11 @@ export default Vue.extend({
]
: [
{ text: this.$t('context.send'), func: this.openConversation },
// { text: this.$t('context.leave_group'), func: this.leaveGroup },
{
text: this.$t('context.leave_group'),
func: this.leaveGroup,
type: 'danger',
},
]
},
lastMessage(): ConversationMessage | undefined {
Expand Down Expand Up @@ -142,7 +147,7 @@ export default Vue.extend({
this.isLoading = false
},
async leaveGroup() {
// todo
iridium.groups.leaveGroup(this.conversation.id)
},
/**
* @method openConversation
Expand Down
10 changes: 4 additions & 6 deletions libraries/Iridium/groups/Group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,6 @@ export default class Group extends Emitter<IridiumMessage> {
return this.state?.members
}

static async getById(id: string, iridium: IridiumManager) {
const group = new Group(id, iridium)
await group.load()
return group
}

async load() {
if (!this.iridium.connector) {
throw new Error(GroupsError.GROUP_NOT_INITIALIZED)
Expand All @@ -45,6 +39,10 @@ export default class Group extends Emitter<IridiumMessage> {
await this.iridium.connector.subscribe(`/groups/${this.id}`)
}

async unsubscribe() {
this.off(`/groups/${this.id}`, this._onWireMessage)
}

private _onWireMessage(message: IridiumPeerMessage<IridiumChannelEvent>) {
const { from, payload } = message
const { channel, data } = payload
Expand Down
153 changes: 151 additions & 2 deletions libraries/Iridium/groups/GroupManager.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,37 @@
import { Emitter, encoding } from '@satellite-im/iridium'
import Vue from 'vue'
import {
didUtils,
Emitter,
encoding,
IridiumGetOptions,
IridiumPeerIdentifier,
} from '@satellite-im/iridium'
import type { IridiumMessage } from '@satellite-im/iridium/src/types'
import { IridiumManager } from '../IridiumManager'
import Group from './Group'
import { GroupConfig, GroupsError } from './types'
import {
GroupConfig,
GroupManagerEvent,
GroupMemberDetails,
GroupsError,
} from './types'
import logger from '~/plugins/local/logger'

export type IridiumGroupEvent = {
to: IridiumPeerIdentifier
status: GroupManagerEvent
at: number
group: Group
member?: GroupMemberDetails
data?: any
}

export default class GroupManager extends Emitter<IridiumMessage> {
groupIds?: string[]
state: { [key: string]: Group } = {}

private loggerTag = 'iridium/groups'

constructor(private readonly iridium: IridiumManager) {
super()
this.iridium = iridium
Expand All @@ -19,13 +43,62 @@ export default class GroupManager extends Emitter<IridiumMessage> {
throw new Error('cannot initialize groups, no iridium connector')
}

iridium.pubsub.on('/groups/announce', this.onGroupsAnnounce.bind(this))

await this.fetch()
}

private async fetch() {
this.state = await this.iridium.connector?.get('/groups')
}

/**
* @method get
* @description get remote state
* @param path string (required)
* @param options object
* @returns iridium's connector result
*/
get<T = any>(path: string, options: IridiumGetOptions = {}): Promise<T> {
if (!this.iridium.connector) {
logger.error(this.loggerTag, 'network error')
throw new Error(GroupsError.NETWORK_ERROR)
}
return this.iridium.connector?.get<T>(
`/groups${path === '/' ? '' : path}`,
options,
)
}

private async onGroupsAnnounce(message: IridiumMessage<IridiumGroupEvent>) {
const { from, payload } = message
const { to, at, status, member, group } = payload.body

if (to !== this.iridium.connector?.id) return
const request = await this.getGroupAnnouncement(from).catch(() => undefined)
switch (status) {
case 'group-member-left':
if (!request) {
logger.warn(
this.loggerTag,
'ignoring group-member-left for unknown group',
)
return
}
await this.removeMemberFromGroup(
group.id,
(member as GroupMemberDetails).id,
)
break
}

return this.state
}

getGroupAnnouncement(did: IridiumPeerIdentifier): Promise<IridiumGroupEvent> {
return this.get<IridiumGroupEvent>(`/announce/${didUtils.didString(did)}`)
}

/**
* @method createGroup
* attempt to create conversation with the provided config. if successful, create a new group too
Expand Down Expand Up @@ -63,11 +136,87 @@ export default class GroupManager extends Emitter<IridiumMessage> {
if (!group) {
throw new Error(GroupsError.GROUP_NOT_FOUND)
}
await group.load()
return group
}

async getGroupMembers(groupId: string) {
const group = await this.getGroup(groupId)
return group.members
}

async send(event: IridiumGroupEvent) {
return this.iridium.connector?.publish(`/groups/announce`, event, {
encrypt: {
recipients: [typeof event.to === 'string' ? event.to : event.to.id],
},
})
}

async leaveGroup(groupId: string) {
const profile = await this.iridium.profile?.get()

if (!this.iridium.connector || !profile) {
logger.error(this.loggerTag, 'network error')
throw new Error(GroupsError.NETWORK_ERROR)
}
// unsubscribe from group chat
const group = await this.getGroup(groupId)
await this.iridium.chat.unsubscribeFromConversation(group.id)

// announce to group members
if (!group.members?.[this.iridium.connector.id]) {
logger.error(this.loggerTag, 'not a member of group')
throw new Error(GroupsError.NOT_A_MEMBER)
}

Vue.delete(group.members, this.iridium.connector.id)

Object.values(group.members).forEach(async (member) => {
const payload: IridiumGroupEvent = {
to: member.id,
status: 'group-member-left',
at: Date.now(),
group,
member: {
id: this.iridium.connector?.id as string,
name: profile?.name,
photoHash: profile?.photoHash,
},
}
logger.info(this.loggerTag, 'announce group left', { groupId, payload })
await this.send(payload)
})

// remove group from local state and iridium
const conversations = this.iridium.chat.state.conversations
Vue.delete(conversations, group.id)
this.iridium.chat.set('/conversations', conversations)
logger.info(this.loggerTag, 'group left', { groupId, group })
}

async removeMemberFromGroup(groupId: string, remotePeerDID: string) {
if (!this.iridium.connector) {
logger.error(this.loggerTag, 'network error')
throw new Error(GroupsError.NETWORK_ERROR)
}

const group = await this.getGroup(groupId)
const members = group.members
if (!members || !members[remotePeerDID]) {
throw new Error(GroupsError.RECIPIENT_NOT_FOUND)
}

Vue.delete(members, remotePeerDID)
await this.iridium.connector.set(`/groups/${groupId}`, {
id: groupId,
origin: this.iridium.connector.id,
group,
})
logger.info(this.loggerTag, 'member removed from group', {
groupId,
memberDid: remotePeerDID,
members,
})
}
}
12 changes: 12 additions & 0 deletions libraries/Iridium/groups/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,19 @@ export type GroupData = GroupConfig & {

export type GroupMap = { [key: string]: Group }

export type GroupManagerEvent =
| 'group-created'
| 'group-updated'
| 'group-deleted'
| 'group-member-added'
| 'group-member-removed'
| 'group-member-updated'
| 'group-member-joined'
| 'group-member-left'

export enum GroupsError {
NETWORK_ERROR = 'errors.groups.network',
NOT_A_MEMBER = 'errors.groups.not_a_member',
GROUP_ALREADY_EXISTS = 'error.groups.group_already_exists',
GROUP_NOT_FOUND = 'error.groups.group_not_found',
GROUP_NOT_INITIALIZED = 'errors.groups.group_not_initialized',
Expand Down

0 comments on commit 3d10d27

Please sign in to comment.