Skip to content

Commit

Permalink
feat(federation): manage invitations through the store
Browse files Browse the repository at this point in the history
Signed-off-by: Maksim Sukharev <antreesy.web@gmail.com>
  • Loading branch information
Antreesy committed Feb 9, 2024
1 parent 9c4899e commit f4b5847
Show file tree
Hide file tree
Showing 4 changed files with 262 additions and 10 deletions.
30 changes: 20 additions & 10 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ import debounce from 'debounce'
import PreventUnload from 'vue-prevent-unload'

import { getCurrentUser } from '@nextcloud/auth'
import axios from '@nextcloud/axios'
import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
import { generateUrl } from '@nextcloud/router'

Expand All @@ -64,6 +63,7 @@ import Router from './router/router.js'
import BrowserStorage from './services/BrowserStorage.js'
import { EventBus } from './services/EventBus.js'
import { leaveConversationSync } from './services/participantsService.js'
import { useFederationStore } from './stores/federation.js'
import { checkBrowser } from './utils/browserCheck.js'
import { signalingKill } from './utils/webrtc/index.js'

Expand All @@ -87,6 +87,7 @@ export default {
isMobile: useIsMobile(),
isNextcloudTalkHashDirty: useHashCheck(),
supportSessionState: useActiveSession(),
federationStore: useFederationStore(),
}
},

Expand Down Expand Up @@ -548,16 +549,24 @@ export default {
// Federation invitation handling
if (event.notification.objectType === 'remote_talk_share') {
try {
const response = await axios.post(event.action.url)
this.$store.dispatch('addConversation', response.data.ocs.data)
this.$router.push({
name: 'conversation',
params: {
token: response.data.ocs.data.token,
},
})

event.cancelAction = true
const conversation = await this.federationStore.acceptShare(event.notification.objectId)
if (conversation.token) {
this.$store.dispatch('addConversation', conversation)
this.$router.push({ name: 'conversation', params: { token: conversation.token } })
}
} catch (error) {
console.error(error)
}
}
break
}
case 'DELETE': {
// Federation invitation handling
if (event.notification.objectType === 'remote_talk_share') {
try {
event.cancelAction = true
this.federationStore.rejectShare(event.notification.objectId)
} catch (error) {
console.error(error)
}
Expand Down Expand Up @@ -600,6 +609,7 @@ export default {
}
// Federation invitation handling
case 'remote_talk_share': {
this.federationStore.addInvitationFromNotification(event.notification)
break
}
default: break
Expand Down
7 changes: 7 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -267,3 +267,10 @@ export const AVATAR = {
FULL: 512,
},
}

export const FEDERATION = {
STATE: {
PENDING: 0,
ACCEPTED: 1,
},
}
59 changes: 59 additions & 0 deletions src/services/federationService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* @copyright Copyright (c) 2024 Maksim Sukharev <antreesy.web@gmail.com>
*
* @author Maksim Sukharev <antreesy.web@gmail.com>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

import axios from '@nextcloud/axios'
import { generateOcsUrl } from '@nextcloud/router'

/**
* Fetches list of shares for a current user
*
* @param {object} [options] options;
*/
const getShares = async function(options) {
return axios.get(generateOcsUrl('apps/spreed/api/v1/federation/invitation', undefined, options), options)
}

/**
* Accept an invitation by provided id.
*
* @param {number} id invitation id;
* @param {object} [options] options;
*/
const acceptShare = async function(id, options) {
return axios.post(generateOcsUrl('apps/spreed/api/v1/federation/invitation/{id}', { id }, options), {}, options)
}

/**
* Reject an invitation by provided id.
*
* @param {number} id invitation id;
* @param {object} [options] options;
*/
const rejectShare = async function(id, options) {
return axios.delete(generateOcsUrl('apps/spreed/api/v1/federation/invitation/{id}', { id }, options), options)
}

export {
getShares,
acceptShare,
rejectShare,
}
176 changes: 176 additions & 0 deletions src/stores/federation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/**
* @copyright Copyright (c) 2024 Maksim Sukharev <antreesy.web@gmail.com>
*
* @author Maksim Sukharev <antreesy.web@gmail.com>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

import { defineStore } from 'pinia'
import Vue from 'vue'

import { FEDERATION } from '../constants.js'
import { getShares, acceptShare, rejectShare } from '../services/federationService.js'

/**
* @typedef {object} Share
* @property {string} accessToken the invitation access token
* @property {number} id the invitation id
* @property {number} localRoomId the invitation local room id
* @property {number} remoteAttendeeId the invitation remote attendee id
* @property {string} remoteServerUrl the invitation remote server URL
* @property {string} remoteToken the invitation remote token
* @property {string} roomName the invitation room name
* @property {number} state the invitation state
* @property {string} userId the invitation user id
*/

/**
* @typedef {object} State
* @property {{[key: string]: Share}} pendingShares - pending invitations
* @property {{[key: string]: Share}} acceptedShares - accepted invitations
*/

/**
* Store for other app integrations (additional actions for messages, participants, e.t.c)
*
* @param {string} id store name
* @param {State} options.state store state structure
*/
export const useFederationStore = defineStore('federation', {
state: () => ({
pendingShares: {},
acceptedShares: {},
}),

actions: {
/**
* Fetch pending invitations and keep them in store
*
*/
async getShares() {
try {
const response = await getShares()
response.data.ocs.data
.forEach(item => {
if (item.state === FEDERATION.STATE.ACCEPTED) {
Vue.set(this.acceptedShares, item.id, item)
} else {
Vue.set(this.pendingShares, item.id, item)
}
})
} catch (error) {
console.error(error)
}
},

/**
* Add an invitation from notification to the store.
*
* @param {number} notification object
*/
addInvitationFromNotification(notification) {
if (this.pendingShares[notification.objectId]) {
return
}
const [remoteServerUrl, remoteToken] = notification.messageRichParameters.roomName.id.split('::')
const invitation = {
accessToken: null,
id: notification.objectId,
localRoomId: null,
remoteAttendeeId: null,
remoteServerUrl,
remoteToken,
roomName: notification.messageRichParameters.roomName.name,
state: FEDERATION.STATE.PENDING,
userId: notification.user,
}
Vue.set(this.pendingShares, invitation.id, invitation)
},

/**
* Mark an invitation as loading in store.
*
* @param {number} id invitation id
* @param {boolean} value loading state
*/
markInvitationLoading(id, value) {
Vue.set(this.pendingShares[id], 'loading', value)
},

/**
* Mark an invitation as accepted in store.
*
* @param {number} id invitation id
* @param {object} conversation conversation object
*/
markInvitationAccepted(id, conversation) {
if (!this.pendingShares[id]) {
return
}
Vue.delete(this.pendingShares[id], 'loading')
Vue.set(this.acceptedShares, id, {
...this.pendingShares[id],
accessToken: conversation.remoteAccessToken,
localRoomId: conversation.id,
state: FEDERATION.STATE.ACCEPTED,
})
Vue.delete(this.pendingShares, id)
},

/**
* Accept an invitation by provided id.
*
* @param {number} id invitation id
* @return {string} conversation to join
*/
async acceptShare(id) {
if (!this.pendingShares[id]) {
return
}
this.markInvitationLoading(id, true)
try {
const response = await acceptShare(id)
this.markInvitationLoading(id, false)
this.markInvitationAccepted(id, response.data.ocs.data)
return response.data.ocs.data
} catch (error) {
console.error(error)
this.markInvitationLoading(id, false)
}
},

/**
* Reject an invitation by provided id.
*
* @param {number} id invitation id
*/
async rejectShare(id) {
if (!this.pendingShares[id]) {
return
}
this.markInvitationLoading(id, true)
try {
await rejectShare(id)
Vue.delete(this.pendingShares, id)
} catch (error) {
console.error(error)
this.markInvitationLoading(id, false)
}
},
},
})

0 comments on commit f4b5847

Please sign in to comment.