Skip to content

Commit

Permalink
chore: merged master branch and resolved conflicts
Browse files Browse the repository at this point in the history
  • Loading branch information
Silver-IT committed Feb 24, 2024
2 parents a1d7144 + 16a36e4 commit 37d656e
Show file tree
Hide file tree
Showing 8 changed files with 283 additions and 241 deletions.
18 changes: 18 additions & 0 deletions backend/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,24 @@ route.GET('/time', {}, function (request, h) {
const MEGABYTE = 1048576 // TODO: add settings for these
const SECOND = 1000

// API endpoint to check for streams support
route.POST('/streams-test', {
payload: {
parse: 'false'
}
},
function (request, h) {
if (
request.payload.byteLength === 2 &&
Buffer.from(request.payload).toString() === 'ok'
) {
return h.response().code(204)
} else {
return Boom.badRequest()
}
}
)

// File upload route.
// If accepted, the file will be stored in Chelonia DB.
route.POST('/file', {
Expand Down
6 changes: 2 additions & 4 deletions frontend/controller/actions/identity.js
Original file line number Diff line number Diff line change
Expand Up @@ -535,10 +535,8 @@ export default (sbp('sbp/selectors/register', {
'gi.actions/identity/shareNewPEK': async (contractID: string, newKeys) => {
const rootState = sbp('state/vuex/state')
const state = rootState[contractID]
const identityContractID = state.attributes.identityContractID

// TODO: Also share PEK with DMs
await Promise.all((state.loginState?.groupIds || []).filter(groupID => !!rootState.contracts[groupID]).map(groupID => {
await Promise.all(Object.keys(state.groups || {}).filter(groupID => !!rootState.contracts[groupID]).map(groupID => {
const CEKid = findKeyIdByName(rootState[groupID], 'cek')
const CSKid = findKeyIdByName(rootState[groupID], 'csk')

Expand Down Expand Up @@ -567,7 +565,7 @@ export default (sbp('sbp/selectors/register', {
hooks: {
preSendCheck: (_, state) => {
// Don't send this message if we're no longer a group member
return state?.profiles?.[identityContractID]?.status === PROFILE_STATUS.ACTIVE
return state?.profiles?.[contractID]?.status === PROFILE_STATUS.ACTIVE
}
}
}).catch(e => {
Expand Down
226 changes: 226 additions & 0 deletions frontend/model/chatroom/vuexModule.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
'use strict'

import sbp from '@sbp/sbp'
import { Vue } from '@common/common.js'
import { merge, cloneDeep, union } from '@model/contracts/shared/giLodash.js'
import { MESSAGE_NOTIFY_SETTINGS, MESSAGE_TYPES } from '@model/contracts/shared/constants.js'
const defaultState = {
currentChatRoomIDs: {}, // { [groupId]: currentChatRoomId }
chatRoomScrollPosition: {}, // [chatRoomId]: messageHash
chatRoomUnread: {}, // [chatRoomId]: { readUntil: { messageHash, createdDate }, messages: [{ messageHash, createdDate, type, deletedDate? }]}
chatNotificationSettings: {} // { messageNotification: MESSAGE_NOTIFY_SETTINGS, messageSound: MESSAGE_NOTIFY_SETTINGS }
}

// getters
const getters = {
currentChatRoomId (state, getters, rootState) {
return state.currentChatRoomIDs[rootState.currentGroupId] || null
},
currentChatRoomState (state, getters, rootState) {
return rootState[getters.currentChatRoomId] || {} // avoid "undefined" vue errors at inoportune times
},
chatNotificationSettings (state) {
return Object.assign({
default: {
messageNotification: MESSAGE_NOTIFY_SETTINGS.DIRECT_MESSAGES,
messageSound: MESSAGE_NOTIFY_SETTINGS.DIRECT_MESSAGES
}
}, state.chatNotificationSettings || {})
},
directMessagesByGroup (state, getters, rootState) {
return groupID => {
const currentGroupDirectMessages = {}
for (const chatRoomId of Object.keys(getters.ourDirectMessages)) {
const chatRoomState = rootState[chatRoomId]
const directMessageSettings = getters.ourDirectMessages[chatRoomId]

// NOTE: skip DMs whose chatroom contracts are not synced yet
if (!chatRoomState || !chatRoomState.members?.[getters.ourIdentityContractId]) {
continue
}
// NOTE: direct messages should be filtered to the ones which are visible and of active group members
const members = Object.keys(chatRoomState.members)
const partners = members
.filter(memberID => memberID !== getters.ourIdentityContractId)
.sort((p1, p2) => {
const p1JoinedDate = new Date(chatRoomState.members[p1].joinedDate).getTime()
const p2JoinedDate = new Date(chatRoomState.members[p2].joinedDate).getTime()
return p1JoinedDate - p2JoinedDate
})
const hasActiveMember = partners.some(memberID => Object.keys(getters.profilesByGroup(groupID)).includes(memberID))
if (directMessageSettings.visible && hasActiveMember) {
// NOTE: lastJoinedParter is chatroom member who has joined the chatroom for the last time.
// His profile picture can be used as the picture of the direct message
// possibly with the badge of the number of partners.
const lastJoinedPartner = partners[partners.length - 1]
currentGroupDirectMessages[chatRoomId] = {
...directMessageSettings,
members,
partners,
lastJoinedPartner,
// TODO: The UI should display display names, usernames and (in the future)
// identity contract IDs differently in some way (e.g., font, font size,
// prefix (@), etc.) to make it impossible (or at least obvious) to impersonate
// users (e.g., 'user1' changing their display name to 'user2')
title: partners.map(cID => getters.userDisplayNameFromID(cID)).join(', '),
picture: getters.ourContactProfiles[lastJoinedPartner]?.picture
}
}
}
return currentGroupDirectMessages
}
},
ourGroupDirectMessages (state, getters, rootState) {
return getters.directMessagesByGroup(rootState.currentGroupId)
},
// NOTE: this getter is used to find the ID of the direct message in the current group
// with the name[s] of partner[s]. Normally it's more useful to find direct message
// by the partners instead of contractID
ourGroupDirectMessageFromUserIds (state, getters) {
return (partners) => { // NOTE: string | string[]
if (typeof partners === 'string') {
partners = [partners]
}
const currentGroupDirectMessages = getters.ourGroupDirectMessages
return Object.keys(currentGroupDirectMessages).find(chatRoomId => {
const cPartners = currentGroupDirectMessages[chatRoomId].partners
return cPartners.length === partners.length && union(cPartners, partners).length === partners.length
})
}
},
isDirectMessage (state, getters) {
// NOTE: identity contract could not be synced at the time of calling this getter
return chatRoomId => !!getters.ourGroupDirectMessages[chatRoomId || getters.currentChatRoomId]
},
isJoinedChatRoom (state, getters, rootState) {
return (chatRoomId: string, memberID?: string) => !!rootState[chatRoomId]?.members?.[memberID || getters.ourIdentityContractId]
},
currentChatVm (state, getters, rootState) {
return rootState?.[getters.currentChatRoomId]?._vm || null
},
currentChatRoomScrollPosition (state, getters) {
return state.chatRoomScrollPosition[getters.currentChatRoomId] // undefined means to the latest
},
ourUnreadMessages (state, getters) {
return state.chatRoomUnread
},
currentChatRoomReadUntil (state, getters) {
// NOTE: Optional Chaining (?) is necessary when user viewing the chatroom which he is not part of
return getters.ourUnreadMessages[getters.currentChatRoomId]?.readUntil // undefined means to the latest
},
chatRoomUnreadMessages (state, getters) {
return (chatRoomId: string) => {
// NOTE: Optional Chaining (?) is necessary when user tries to get mentions of the chatroom which he is not part of
return getters.ourUnreadMessages[chatRoomId]?.messages || []
}
},
chatRoomUnreadMentions (state, getters) {
return (chatRoomId: string) => {
// NOTE: Optional Chaining (?) is necessary when user tries to get mentions of the chatroom which he is not part of
return (getters.ourUnreadMessages[chatRoomId]?.messages || []).filter(m => m.type === MESSAGE_TYPES.TEXT)
}
},
groupUnreadMessages (state, getters, rootState) {
return (groupID: string) => {
const isGroupDirectMessage = cID => Object.keys(getters.directMessagesByGroup(groupID)).includes(cID)
const isGroupChatroom = cID => Object.keys(state[groupID]?.chatRooms || {}).includes(cID)
return Object.keys(getters.ourUnreadMessages)
.filter(cID => isGroupDirectMessage(cID) || isGroupChatroom(cID))
.map(cID => getters.ourUnreadMessages[cID].messages.length)
.reduce((sum, n) => sum + n, 0)
}
},
groupIdFromChatRoomId (state, getters, rootState) {
return (chatRoomId: string) => Object.keys(rootState.contracts)
.find(cId => rootState.contracts[cId].type === 'gi.contracts/group' &&
Object.keys(rootState[cId].chatRooms).includes(chatRoomId))
},
chatRoomsInDetail (state, getters, rootState) {
const chatRoomsInDetail = merge({}, getters.getGroupChatRooms)
for (const contractID in chatRoomsInDetail) {
const chatRoom = rootState[contractID]
if (chatRoom && chatRoom.attributes &&
chatRoom.members[rootState.loggedIn.identityContractID]) {
chatRoomsInDetail[contractID] = {
...chatRoom.attributes,
id: contractID,
unreadMessagesCount: getters.chatRoomUnreadMessages(contractID).length,
joined: true
}
} else {
const { name, privacyLevel } = chatRoomsInDetail[contractID]
chatRoomsInDetail[contractID] = { id: contractID, name, privacyLevel, joined: false }
}
}
return chatRoomsInDetail
},
chatRoomMembersInSort (state, getters) {
return getters.groupMembersSorted
.map(member => ({ contractID: member.contractID, username: member.username, displayName: member.displayName }))
.filter(member => !!getters.chatRoomMembers[member.contractID]) || []
}
}

// mutations
const mutations = {
setCurrentChatRoomId (state, { groupId, chatRoomId }) {
const rootState = sbp('state/vuex/state')

if (groupId && state[groupId] && chatRoomId) { // useful when initialize when syncing in another device
Vue.set(state.currentChatRoomIDs, groupId, chatRoomId)
} else if (chatRoomId) { // set chatRoomId as the current chatroomId of current group
Vue.set(state.currentChatRoomIDs, rootState.currentGroupId, chatRoomId)
} else if (groupId && rootState[groupId]) { // set defaultChatRoomId as the current chatroomId of current group
Vue.set(state.currentChatRoomIDs, rootState.currentGroupId, rootState[groupId].generalChatRoomId || null)
} else { // reset
Vue.set(state.currentChatRoomIDs, rootState.currentGroupId, null)
}
},
setChatRoomScrollPosition (state, { chatRoomId, messageHash }) {
Vue.set(state.chatRoomScrollPosition, chatRoomId, messageHash)
},
deleteChatRoomScrollPosition (state, { chatRoomId }) {
Vue.delete(state.chatRoomScrollPosition, chatRoomId)
},
setChatRoomReadUntil (state, { chatRoomId, messageHash, createdDate }) {
Vue.set(state.chatRoomUnread, chatRoomId, {
readUntil: { messageHash, createdDate, deletedDate: null },
messages: state.chatRoomUnread[chatRoomId].messages
?.filter(m => new Date(m.createdDate).getTime() > new Date(createdDate).getTime()) || []
})
},
deleteChatRoomReadUntil (state, { chatRoomId, deletedDate }) {
if (state.chatRoomUnread[chatRoomId].readUntil) {
Vue.set(state.chatRoomUnread[chatRoomId].readUntil, 'deletedDate', deletedDate)
}
},
addChatRoomUnreadMessage (state, { chatRoomId, messageHash, createdDate, type }) {
state.chatRoomUnread[chatRoomId].messages.push({ messageHash, createdDate, type })
},
deleteChatRoomUnreadMessage (state, { chatRoomId, messageHash }) {
Vue.set(
state.chatRoomUnread[chatRoomId],
'messages',
state.chatRoomUnread[chatRoomId].messages.filter(m => m.messageHash !== messageHash)
)
},
deleteChatRoomUnread (state, { chatRoomId }) {
Vue.delete(state.chatRoomUnread, chatRoomId)
},
setChatroomNotificationSettings (state, { chatRoomId, settings }) {
if (chatRoomId) {
if (!state.chatNotificationSettings[chatRoomId]) {
Vue.set(state.chatNotificationSettings, chatRoomId, {})
}
for (const key in settings) {
Vue.set(state.chatNotificationSettings[chatRoomId], key, settings[key])
}
}
}
}

export default ({
state: () => cloneDeep(defaultState),
getters,
mutations
}: Object)
6 changes: 3 additions & 3 deletions frontend/model/contracts/chatroom.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ sbp('chelonia/defineContract', {
}
},
sideEffect ({ contractID }) {
Vue.set(sbp('state/vuex/state').chatRoomUnread, contractID, {
Vue.set(sbp('state/vuex/state')?.chatroom?.chatRoomUnread, contractID, {
readUntil: undefined,
messages: []
})
Expand Down Expand Up @@ -519,15 +519,15 @@ sbp('chelonia/defineContract', {
const rootState = sbp('state/vuex/state')
const me = rootState.loggedIn.identityContractID

if (rootState.chatRoomScrollPosition[contractID] === data.hash) {
if (rootState.chatroom.chatRoomScrollPosition[contractID] === data.hash) {
sbp('state/vuex/commit', 'setChatRoomScrollPosition', {
chatRoomId: contractID, messageHash: null
})
}

// NOTE: readUntil can't be undefined because it would be set in advance
// while syncing the contracts events especially join, addMessage, ...
if (rootState.chatRoomUnread[contractID].readUntil.messageHash === data.hash) {
if (rootState.chatroom.chatRoomUnread[contractID].readUntil.messageHash === data.hash) {
sbp('state/vuex/commit', 'deleteChatRoomReadUntil', {
chatRoomId: contractID,
deletedDate: meta.createdDate
Expand Down
7 changes: 3 additions & 4 deletions frontend/model/contracts/group.js
Original file line number Diff line number Diff line change
Expand Up @@ -1415,7 +1415,7 @@ sbp('chelonia/defineContract', {
sbp('okTurtles.data/delete', `JOINING_CHATROOM-${data.chatRoomID}-${memberID}`)
sbp('chelonia/contract/remove', data.chatRoomID).then(() => {
const rootState = sbp('state/vuex/state')
if (rootState.currentChatRoomIDs[contractID] === data.chatRoomID) {
if (rootState.chatroom.currentChatRoomIDs[contractID] === data.chatRoomID) {
sbp('state/vuex/commit', 'setCurrentChatRoomId', {
groupId: contractID
})
Expand Down Expand Up @@ -1915,9 +1915,8 @@ sbp('chelonia/defineContract', {

Promise.resolve()
.then(() => sbp('gi.contracts/group/rotateKeys', contractID))
.then(() => {
return sbp('gi.contracts/group/revokeGroupKeyAndRotateOurPEK', contractID, false)
}).catch((e) => {
.then(() => sbp('gi.contracts/group/revokeGroupKeyAndRotateOurPEK', contractID, false))
.catch((e) => {
console.error(`[gi.contracts/group/leaveGroup] for ${contractID}: Error rotating group keys or our PEK`, e)
})

Expand Down
Loading

0 comments on commit 37d656e

Please sign in to comment.