Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

"Modified since" support on roomlist #8726

Merged
merged 3 commits into from Feb 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 19 additions & 7 deletions docs/conversation.md
Expand Up @@ -13,14 +13,25 @@
It is therefor recommended to use `format=json` or send the `Accept: application/json` header,
to receive a JSON response.

!!! note

When `modifiedSince` is provided only conversations with a newer `lastActivity` are returned.
Due to the nature of the data structure we can not return information that a conversation was deleted
or the user removed from a conversation. Therefore it is recommended to do a full refresh:
* Every 5 minutes
* When a signaling message of type `delete` or `disinvite` was received
* Internal signaling mode is used


* Method: `GET`
* Endpoint: `/room`
* Data:

| field | type | Description |
|------------------|------|------------------------------------------------------------------------------------------------------------------|
| `noStatusUpdate` | int | Whether the "online" user status of the current user should be "kept-alive" (`1`) or not (`0`) (defaults to `0`) |
| `includeStatus` | bool | Whether the user status information of all one-to-one conversations should be loaded (default false) |
| field | type | Description |
|------------------|------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `noStatusUpdate` | int | Whether the "online" user status of the current user should be "kept-alive" (`1`) or not (`0`) (defaults to `0`) |
| `includeStatus` | bool | Whether the user status information of all one-to-one conversations should be loaded (default false) |
| `modifiedSince` | int | **Use with care as per note above.** When provided only conversations with a newer `lastActivity` (and one-to-one conversations when `includeStatus` is provided) are returned. |

* Response:
- Status code:
Expand All @@ -29,9 +40,10 @@

- Header:

| field | type | Description |
|-------------------------|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------|
| `X-Nextcloud-Talk-Hash` | string | Sha1 value over some config. When you receive a different value on subsequent requests, the capabilities and the signaling settings should be refreshed. |
| field | type | Description |
|------------------------------------|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------|
| `X-Nextcloud-Talk-Hash` | string | Sha1 value over some config. When you receive a different value on subsequent requests, the capabilities and the signaling settings should be refreshed. |
| `X-Nextcloud-Talk-Modified-Before` | string | Timestamp from before the database request that can be used as `modifiedSince` parameter in the next request |

- Data:
Array of conversations, each conversation has at least:
Expand Down
16 changes: 14 additions & 2 deletions lib/Controller/RoomController.php
Expand Up @@ -173,7 +173,9 @@ protected function getTalkHashHeader(): array {
* @param bool $includeStatus
* @return DataResponse
*/
public function getRooms(int $noStatusUpdate = 0, bool $includeStatus = false): DataResponse {
public function getRooms(int $noStatusUpdate = 0, bool $includeStatus = false, int $modifiedSince = 0): DataResponse {
$nextModifiedSince = $this->timeFactory->getTime();

$event = new UserEvent($this->userId);
$this->dispatcher->dispatch(self::EVENT_BEFORE_ROOMS_GET, $event);

Expand All @@ -196,6 +198,14 @@ public function getRooms(int $noStatusUpdate = 0, bool $includeStatus = false):

$sessionIds = $this->session->getAllActiveSessions();
$rooms = $this->manager->getRoomsForUser($this->userId, $sessionIds, true);

if ($modifiedSince !== 0) {
$rooms = array_filter($rooms, static function (Room $room) use ($includeStatus, $modifiedSince): bool {
return ($includeStatus && $room->getType() === Room::TYPE_ONE_TO_ONE)
|| ($room->getLastActivity() && $room->getLastActivity()->getTimestamp() >= $modifiedSince);
});
}

$readPrivacy = $this->talkConfig->getUserReadPrivacy($this->userId);
if ($readPrivacy === Participant::PRIVACY_PUBLIC) {
$roomIds = array_map(static function (Room $room) {
Expand Down Expand Up @@ -233,7 +243,9 @@ public function getRooms(int $noStatusUpdate = 0, bool $includeStatus = false):
}
}

return new DataResponse($return, Http::STATUS_OK, $this->getTalkHashHeader());
$response = new DataResponse($return, Http::STATUS_OK, $this->getTalkHashHeader());
$response->addHeader('X-Nextcloud-Talk-Modified-Before', (string) $nextModifiedSince);
return $response;
}

/**
Expand Down
8 changes: 4 additions & 4 deletions src/components/LeftSidebar/LeftSidebar.spec.js
Expand Up @@ -76,7 +76,7 @@ describe('LeftSidebar.vue', () => {

// note: need a copy because the Vue modifies it when sorting
conversationsListMock = jest.fn()
fetchConversationsAction = jest.fn()
fetchConversationsAction = jest.fn().mockReturnValue({ headers: {} })
addConversationAction = jest.fn()
createOneToOneConversationAction = jest.fn()
const getUserIdMock = jest.fn().mockReturnValue('current-user')
Expand Down Expand Up @@ -131,7 +131,7 @@ describe('LeftSidebar.vue', () => {

const wrapper = mountComponent()

expect(fetchConversationsAction).toHaveBeenCalledWith(expect.anything(), undefined)
expect(fetchConversationsAction).toHaveBeenCalledWith(expect.anything(), expect.anything())
expect(conversationsListMock).toHaveBeenCalled()

const appNavEl = wrapper.findComponent({ name: 'NcAppNavigation' })
Expand Down Expand Up @@ -194,7 +194,7 @@ describe('LeftSidebar.vue', () => {

expect(fetchConversationsAction).not.toHaveBeenCalled()

EventBus.$emit('should-refresh-conversations')
EventBus.$emit('should-refresh-conversations', {})

// note: debounce was short-circuited so no delay needed
expect(fetchConversationsAction).toHaveBeenCalled()
Expand Down Expand Up @@ -313,7 +313,7 @@ describe('LeftSidebar.vue', () => {

const wrapper = mountComponent()

expect(fetchConversationsAction).toHaveBeenCalledWith(expect.anything(), undefined)
expect(fetchConversationsAction).toHaveBeenCalledWith(expect.anything(), expect.anything())
expect(conversationsListMock).toHaveBeenCalled()

const appNavEl = wrapper.findComponent({ name: 'NcAppNavigation' })
Expand Down
29 changes: 26 additions & 3 deletions src/components/LeftSidebar/LeftSidebar.vue
Expand Up @@ -178,6 +178,8 @@ export default {
unreadNum: 0,
firstUnreadPos: 0,
preventFindingUnread: false,
roomListModifiedBefore: 0,
forceFullRoomListRefreshAfterXLoops: 0,
}
},

Expand Down Expand Up @@ -439,8 +441,10 @@ export default {
return conversation2.lastActivity - conversation1.lastActivity
},

async handleShouldRefreshConversations(token, properties) {
if (token && properties) {
async handleShouldRefreshConversations({ token = undefined, properties = undefined, all = undefined }) {
if (all === true) {
this.roomListModifiedBefore = 0
} else if (token && properties) {
await this.$store.dispatch('setConversationProperties', { token, properties })
}

Expand All @@ -455,13 +459,32 @@ export default {

async fetchConversations() {
this.isFetchingConversations = true
if (this.forceFullRoomListRefreshAfterXLoops === 0) {
this.roomListModifiedBefore = 0
this.forceFullRoomListRefreshAfterXLoops = 10
} else {
this.forceFullRoomListRefreshAfterXLoops--
}

/**
* Fetches the conversations from the server and then adds them one by one
* to the store.
*/
try {
await this.$store.dispatch('fetchConversations')
const response = await this.$store.dispatch('fetchConversations', {
modifiedSince: this.roomListModifiedBefore,
})

// We can only support this with the HPB as otherwise rooms,
// you are not currently active in, will not be removed anymore,
// as there is no signaling message about it when the internal
// signaling is used.
if (loadState('spreed', 'signaling_mode') !== 'internal') {
if (response?.headers && response.headers['x-nextcloud-talk-modified-before']) {
this.roomListModifiedBefore = response.headers['x-nextcloud-talk-modified-before']
}
}

this.initialisedConversations = true
/**
* Emits a global event that is used in App.vue to update the page title once the
Expand Down
18 changes: 15 additions & 3 deletions src/store/conversationsStore.js
Expand Up @@ -604,13 +604,25 @@ const actions = {
}
},

async fetchConversations({ dispatch }) {
async fetchConversations({ dispatch }, { modifiedSince }) {
try {
dispatch('clearMaintenanceMode')
modifiedSince = modifiedSince || 0

let options = {}
if (modifiedSince !== 0) {
options = {
params: {
modifiedSince,
},
}
}

const response = await fetchConversations()
const response = await fetchConversations(options)
dispatch('updateTalkVersionHash', response)
dispatch('purgeConversationsStore')
if (modifiedSince === 0) {
dispatch('purgeConversationsStore')
}
response.data.ocs.data.forEach(conversation => {
dispatch('addConversation', conversation)
})
Expand Down
6 changes: 3 additions & 3 deletions src/store/conversationsStore.spec.js
Expand Up @@ -259,9 +259,9 @@ describe('conversationsStore', () => {

fetchConversations.mockResolvedValue(response)

await store.dispatch('fetchConversations')
await store.dispatch('fetchConversations', {})

expect(fetchConversations).toHaveBeenCalledWith()
expect(fetchConversations).toHaveBeenCalledWith({})
expect(store.getters.conversationsList).toStrictEqual(testConversations)

expect(clearMaintenanceModeAction).toHaveBeenCalled()
Expand All @@ -281,7 +281,7 @@ describe('conversationsStore', () => {
const response = { status: 503 }
fetchConversations.mockRejectedValue({ response })

await expect(store.dispatch('fetchConversations')).rejects.toMatchObject({ response })
await expect(store.dispatch('fetchConversations', {})).rejects.toMatchObject({ response })

expect(checkMaintenanceModeAction).toHaveBeenCalledWith(expect.anything(), response)
})
Expand Down
9 changes: 8 additions & 1 deletion src/utils/signaling.js
Expand Up @@ -1302,6 +1302,10 @@ Signaling.Standalone.prototype.processRoomMessageEvent = function(data) {

Signaling.Standalone.prototype.processRoomListEvent = function(data) {
switch (data.event.type) {
case 'delete':
console.debug('Room list event', data)
EventBus.$emit('should-refresh-conversations', { all: true })
break
case 'update':
if (data.event.update.properties['participant-list']) {
console.debug('Room list event for participant list', data)
Expand Down Expand Up @@ -1336,7 +1340,10 @@ Signaling.Standalone.prototype.processRoomListEvent = function(data) {
normalizedProperties[normalizedKey] = properties[key]
})

EventBus.$emit('should-refresh-conversations', data.event.update.roomid, normalizedProperties)
EventBus.$emit('should-refresh-conversations', {
token: data.event.update.roomid,
properties: normalizedProperties,
})
break
}
// eslint-disable-next-line no-fallthrough
Expand Down