Browse Source

Merge pull request #3993 from nextcloud/feature/noid/status

💬 Status adaption for Nextcloud 20
pull/3995/head
Joas Schilling 5 years ago
committed by GitHub
parent
commit
afeb321ff0
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      css/At.scss
  2. 4
      docs/chat.md
  3. 8
      docs/participant.md
  4. 38
      lib/Controller/ChatController.php
  5. 28
      lib/Controller/RoomController.php
  6. 12
      src/components/NewMessageForm/AdvancedInput/AdvancedInput.vue
  7. 12
      src/components/RightSidebar/Participants/CurrentParticipants/CurrentParticipants.vue
  8. 33
      src/components/RightSidebar/Participants/ParticipantsList/Participant/Participant.vue
  9. 56
      src/mixins/userStatus.js
  10. 1
      src/services/mentionsService.js
  11. 3
      src/services/participantsService.js
  12. 5
      tests/php/Controller/ChatControllerTest.php
  13. 5
      tests/php/Controller/RoomControllerTest.php

4
css/At.scss

@ -150,8 +150,8 @@
}
.atwho-cur {
background: var(--color-primary);
color: var(--color-primary-text);
background: var(--color-primary-light);
color: var(--color-main-text);
}
}
}

4
docs/chat.md

@ -116,6 +116,7 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1`
------|------|------------
`search` | string | Search term for name suggestions (should at least be 1 character)
`limit` | int | Number of suggestions to receive (20 by default)
`includeStatus` | bool | Whether the user status information also needs to be loaded
* Response:
- Status code:
@ -132,6 +133,9 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1`
`id` | string | The user id which should be sent as `@<id>` in the message (user ids that contain spaces as well as guest ids need to be wrapped in double-quotes when sending in a message: `@"space user"` and `@"guest/random-string"`)
`label` | string | The displayname of the user
`source` | string | The type of the user, currently only `users`, `guests` or `calls` (for mentioning the whole conversation
`status` | string | Optional: Only available with `includeStatus=true` and for users with a set status
`statusIcon` | string | Optional: Only available with `includeStatus=true` and for users with a set status
`statusMessage` | string | Optional: Only available with `includeStatus=true` and for users with a set status
## System messages

8
docs/participant.md

@ -6,6 +6,11 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1`
* Method: `GET`
* Endpoint: `/room/{token}/participants`
* Data:
field | type | Description
------|------|------------
`includeStatus` | bool | Whether the user status information also needs to be loaded
* Response:
- Status code:
@ -24,6 +29,9 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1`
`participantType` | int | Permissions level of the participant
`lastPing` | int | Timestamp of the last ping of the user (should be used for sorting)
`sessionId` | string | `'0'` if not connected, otherwise a 512 character long string
`status` | string | Optional: Only available with `includeStatus=true` and for users with a set status
`statusIcon` | string | Optional: Only available with `includeStatus=true` and for users with a set status
`statusMessage` | string | Optional: Only available with `includeStatus=true` and for users with a set status
## Add a participant to a conversation

38
lib/Controller/ChatController.php

@ -44,6 +44,8 @@ use OCP\Comments\NotFoundException;
use OCP\IL10N;
use OCP\IRequest;
use OCP\IUserManager;
use OCP\UserStatus\IManager as IUserStatusManager;
use OCP\UserStatus\IUserStatus;
class ChatController extends AEnvironmentAwareController {
@ -71,6 +73,9 @@ class ChatController extends AEnvironmentAwareController {
/** @var IManager */
private $autoCompleteManager;
/** @var IUserStatusManager */
private $statusManager;
/** @var SearchPlugin */
private $searchPlugin;
@ -91,6 +96,7 @@ class ChatController extends AEnvironmentAwareController {
GuestManager $guestManager,
MessageParser $messageParser,
IManager $autoCompleteManager,
IUserStatusManager $statusManager,
SearchPlugin $searchPlugin,
ISearchResult $searchResult,
ITimeFactory $timeFactory,
@ -104,6 +110,7 @@ class ChatController extends AEnvironmentAwareController {
$this->guestManager = $guestManager;
$this->messageParser = $messageParser;
$this->autoCompleteManager = $autoCompleteManager;
$this->statusManager = $statusManager;
$this->searchPlugin = $searchPlugin;
$this->searchResult = $searchResult;
$this->timeFactory = $timeFactory;
@ -397,9 +404,10 @@ class ChatController extends AEnvironmentAwareController {
*
* @param string $search
* @param int $limit
* @param bool $includeStatus
* @return DataResponse
*/
public function mentions(string $search, int $limit = 20): DataResponse {
public function mentions(string $search, int $limit = 20, bool $includeStatus = false): DataResponse {
$this->searchPlugin->setContext([
'itemType' => 'chat',
'itemId' => $this->room->getId(),
@ -419,7 +427,16 @@ class ChatController extends AEnvironmentAwareController {
'search' => $search,
]);
$results = $this->prepareResultArray($results);
$statuses = [];
if ($includeStatus) {
$userIds = array_filter(array_map(static function (array $userResult) {
return $userResult['value']['shareWith'];
}, $results['users']));
$statuses = $this->statusManager->getUserStatuses($userIds);
}
$results = $this->prepareResultArray($results, $statuses);
$roomDisplayName = $this->room->getDisplayName($this->participant->getUser());
if (($search === '' || strpos('all', $search) !== false || stripos($roomDisplayName, $search) !== false) && $this->room->getType() !== Room::ONE_TO_ONE_CALL) {
@ -444,15 +461,28 @@ class ChatController extends AEnvironmentAwareController {
}
protected function prepareResultArray(array $results): array {
/**
* @param array $results
* @param IUserStatus[] $statuses
* @return array
*/
protected function prepareResultArray(array $results, array $statuses): array {
$output = [];
foreach ($results as $type => $subResult) {
foreach ($subResult as $result) {
$output[] = [
$data = [
'id' => $result['value']['shareWith'],
'label' => $result['label'],
'source' => $type,
];
if ($type === 'users' && isset($statuses[$data['id']])) {
$data['status'] = $statuses[$data['id']]->getStatus();
$data['statusIcon'] = $statuses[$data['id']]->getIcon();
$data['statusMessage'] = $statuses[$data['id']]->getMessage();
}
$output[] = $data;
}
}
return $output;

28
lib/Controller/RoomController.php

@ -58,6 +58,7 @@ use OCP\IUserManager;
use OCP\IGroup;
use OCP\IGroupManager;
use OCP\IConfig;
use OCP\UserStatus\IManager as IUserStatusManager;
class RoomController extends AEnvironmentAwareController {
public const EVENT_BEFORE_ROOMS_GET = self::class . '::preGetRooms';
@ -78,6 +79,8 @@ class RoomController extends AEnvironmentAwareController {
protected $roomService;
/** @var GuestManager */
protected $guestManager;
/** @var IUserStatusManager */
protected $statusManager;
/** @var ChatManager */
protected $chatManager;
/** @var IEventDispatcher */
@ -103,6 +106,7 @@ class RoomController extends AEnvironmentAwareController {
Manager $manager,
RoomService $roomService,
GuestManager $guestManager,
IUserStatusManager $statusManager,
ChatManager $chatManager,
IEventDispatcher $dispatcher,
MessageParser $messageParser,
@ -119,6 +123,7 @@ class RoomController extends AEnvironmentAwareController {
$this->manager = $manager;
$this->roomService = $roomService;
$this->guestManager = $guestManager;
$this->statusManager = $statusManager;
$this->chatManager = $chatManager;
$this->dispatcher = $dispatcher;
$this->messageParser = $messageParser;
@ -824,9 +829,10 @@ class RoomController extends AEnvironmentAwareController {
* @RequireParticipant
* @RequireModeratorOrNoLobby
*
* @param bool $includeStatus
* @return DataResponse
*/
public function getParticipants(): DataResponse {
public function getParticipants(bool $includeStatus = false): DataResponse {
if ($this->participant->getParticipantType() === Participant::GUEST) {
return new DataResponse([], Http::STATUS_FORBIDDEN);
}
@ -835,6 +841,12 @@ class RoomController extends AEnvironmentAwareController {
$participants = $this->room->getParticipantsLegacy();
$results = [];
$statuses = [];
if ($includeStatus && count($participants['users']) < 100) {
$userIds = array_map('strval', array_keys($participants['users']));
$statuses = $this->statusManager->getUserStatuses($userIds);
}
foreach ($participants['users'] as $userId => $participant) {
$userId = (string) $userId;
if ($participant['sessionId'] !== '0' && $participant['lastPing'] <= $maxPingAge) {
@ -846,10 +858,16 @@ class RoomController extends AEnvironmentAwareController {
continue;
}
$results[] = array_merge($participant, [
'userId' => $userId,
'displayName' => (string) $user->getDisplayName(),
]);
$participant['userId'] = $userId;
$participant['displayName'] = (string) $user->getDisplayName();
if (isset($statuses[$userId])) {
$participant['status'] = $statuses[$userId]->getStatus();
$participant['statusIcon'] = $statuses[$userId]->getIcon();
$participant['statusMessage'] = $statuses[$userId]->getMessage();
}
$results[] = $participant;
}
$guestSessions = [];

12
src/components/NewMessageForm/AdvancedInput/AdvancedInput.vue

@ -46,6 +46,8 @@
:disable-menu="true" />
&nbsp;
<span>{{ scope.item.label }}</span>
<em v-if="isNotAvailable(scope.item)">&nbsp;{{ getStatus(scope.item) }}</em>
</template>
<template v-slot:embeddedItem="scope">
<!-- The root element itself is ignored, only its contents are taken
@ -76,6 +78,7 @@
<script>
import At from 'vue-at'
import UserStatus from '../../../mixins/userStatus'
import VueAtReparenter from '../../../mixins/vueAtReparenter'
import { EventBus } from '../../../services/EventBus'
import { searchPossibleMentions } from '../../../services/mentionsService'
@ -153,6 +156,7 @@ export default {
},
mixins: [
VueAtReparenter,
UserStatus,
],
props: {
/**
@ -350,6 +354,14 @@ export default {
return 'user'
},
getStatus(candidate) {
const status = this.getStatusMessage(candidate)
if (status) {
return '(' + status + ')'
}
return ''
},
},
}
</script>

12
src/components/RightSidebar/Participants/CurrentParticipants/CurrentParticipants.vue

@ -29,6 +29,7 @@
import ParticipantsList from '../ParticipantsList/ParticipantsList'
import { PARTICIPANT } from '../../../../constants'
import UserStatus from '../../../../mixins/userStatus'
export default {
name: 'CurrentParticipants',
@ -37,6 +38,10 @@ export default {
ParticipantsList,
},
mixins: [
UserStatus,
],
props: {
searchText: {
type: String,
@ -76,6 +81,7 @@ export default {
* Sort two participants by:
* - type (moderators before normal participants)
* - online status
* - user status (away + dnd at the end)
* - display name
*
* @param {object} participant1 First participant
@ -105,6 +111,12 @@ export default {
return -1
}
const participant1Away = this.isNotAvailable(participant1)
const participant2Away = this.isNotAvailable(participant2)
if (participant1Away !== participant2Away) {
return participant1Away ? 1 : -1
}
return participant1.displayName.localeCompare(participant2.displayName)
},
},

33
src/components/RightSidebar/Participants/ParticipantsList/Participant/Participant.vue

@ -33,10 +33,18 @@
:name="computedName"
:source="participant.source"
:offline="isOffline" />
<span class="participant-row__user-name">{{ computedName }}</span>
<span v-if="showModeratorLabel" class="participant-row__moderator-indicator">({{ t('spreed', 'moderator') }})</span>
<span v-if="isGuest" class="participant-row__guest-indicator">({{ t('spreed', 'guest') }})</span>
<span v-if="callIconClass" class="icon callstate-icon" :class="callIconClass" />
<div class="participant-row__user-wrapper">
<div class="participant-row__user-descriptor">
<span class="participant-row__user-name">{{ computedName }}</span>
<span v-if="showModeratorLabel" class="participant-row__moderator-indicator">({{ t('spreed', 'moderator') }})</span>
<span v-if="isGuest" class="participant-row__guest-indicator">({{ t('spreed', 'guest') }})</span>
<span v-if="callIconClass" class="icon callstate-icon" :class="callIconClass" />
</div>
<div v-if="isNotAvailable(participant)"
class="participant-row__status">
<span>{{ getStatusMessage(participant) }}</span>
</div>
</div>
<Actions
v-if="canModerate && !isSearched"
:aria-label="t('spreed', 'Participant settings')"
@ -66,6 +74,7 @@
import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
import Actions from '@nextcloud/vue/dist/Components/Actions'
import { CONVERSATION, PARTICIPANT } from '../../../../../constants'
import UserStatus from '../../../../../mixins/userStatus'
import isEqual from 'lodash/isEqual'
import AvatarWrapper from '../../../../AvatarWrapper/AvatarWrapper'
@ -78,6 +87,10 @@ export default {
AvatarWrapper,
},
mixins: [
UserStatus,
],
props: {
participant: {
type: Object,
@ -277,11 +290,13 @@ export default {
height: 44px;
cursor: pointer;
padding: 0 5px;
margin: 5px 0;
margin: 8px 0;
border-radius: 22px;
&__user-wrapper {
padding-left: 8px;
}
&__user-name {
margin-left: 6px;
display: inline-block;
vertical-align: middle;
line-height: normal;
@ -293,6 +308,10 @@ export default {
font-weight: 300;
padding-left: 5px;
}
&__status {
color: var(--color-text-maxcontrast);
line-height: 1.3em;
}
&__icon {
width: 32px;
height: 44px;
@ -317,7 +336,7 @@ export default {
.offline {
& > span {
.participant-row__user-descriptor > span {
color: var(--color-text-maxcontrast);
}
}

56
src/mixins/userStatus.js

@ -0,0 +1,56 @@
/**
* @copyright Copyright (c) 2020 Marco Ambrosini <marcoambrosini@pm.me>
*
* @author Marco Ambrosini <marcoambrosini@pm.me>
*
* @license GNU AGPL version 3 or any later version
*
* 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/>.
*
*/
const userStatus = {
methods: {
getStatusMessage(userData) {
if (!this.isNotAvailable(userData)) {
return ''
}
let status = ''
if (userData.statusIcon) {
status = userData.statusIcon + ' '
}
if (userData.statusMessage) {
status += userData.statusMessage
} else if (userData.status === 'dnd') {
status += t('spreed', 'Do not disturb')
} else {
status += t('spreed', 'Away')
}
return status
},
isNotAvailable(userData) {
if (!userData.status) {
return false
}
return userData.status === 'away' || userData.status === 'dnd'
},
},
}
export default userStatus

1
src/services/mentionsService.js

@ -33,6 +33,7 @@ const searchPossibleMentions = async function(token, searchText) {
const response = await axios.get(generateOcsUrl('apps/spreed/api/v1/chat', 2) + `${token}/mentions`, {
params: {
search: searchText,
includeStatus: 1,
},
})
return response

3
src/services/participantsService.js

@ -223,6 +223,9 @@ const demoteFromModerator = async(token, options) => {
}
const fetchParticipants = async(token, options) => {
options = options || {}
options.params = options.params || {}
options.params.includeStatus = true
const response = await axios.get(generateOcsUrl('apps/spreed/api/v2/room', 2) + token + '/participants', options)
return response
}

5
tests/php/Controller/ChatControllerTest.php

@ -42,6 +42,7 @@ use OCP\IL10N;
use OCP\IRequest;
use OCP\IUser;
use OCP\IUserManager;
use OCP\UserStatus\IManager as IUserStatusManager;
use PHPUnit\Framework\Constraint\Callback;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
@ -62,6 +63,8 @@ class ChatControllerTest extends TestCase {
protected $messageParser;
/** @var IManager|MockObject */
protected $autoCompleteManager;
/** @var IUserStatusManager|MockObject */
protected $statusManager;
/** @var SearchPlugin|MockObject */
protected $searchPlugin;
/** @var ISearchResult|MockObject */
@ -90,6 +93,7 @@ class ChatControllerTest extends TestCase {
$this->guestManager = $this->createMock(GuestManager::class);
$this->messageParser = $this->createMock(MessageParser::class);
$this->autoCompleteManager = $this->createMock(IManager::class);
$this->statusManager = $this->createMock(IUserStatusManager::class);
$this->searchPlugin = $this->createMock(SearchPlugin::class);
$this->searchResult = $this->createMock(ISearchResult::class);
$this->timeFactory = $this->createMock(ITimeFactory::class);
@ -118,6 +122,7 @@ class ChatControllerTest extends TestCase {
$this->guestManager,
$this->messageParser,
$this->autoCompleteManager,
$this->statusManager,
$this->searchPlugin,
$this->searchResult,
$this->timeFactory,

5
tests/php/Controller/RoomControllerTest.php

@ -43,6 +43,7 @@ use OCP\IGroupManager;
use OCP\IL10N;
use OCP\IRequest;
use OCP\IUserManager;
use OCP\UserStatus\IManager as IUserStatusManager;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
@ -58,6 +59,8 @@ class RoomControllerTest extends TestCase {
protected $userManager;
/** @var IGroupManager|MockObject */
protected $groupManager;
/** @var IUserStatusManager|MockObject */
protected $statusManager;
/** @var Manager|MockObject */
protected $manager;
/** @var RoomService|MockObject */
@ -91,6 +94,7 @@ class RoomControllerTest extends TestCase {
$this->manager = $this->createMock(Manager::class);
$this->roomService = $this->createMock(RoomService::class);
$this->guestManager = $this->createMock(GuestManager::class);
$this->statusManager = $this->createMock(IUserStatusManager::class);
$this->chatManager = $this->createMock(ChatManager::class);
$this->dispatcher = $this->createMock(IEventDispatcher::class);
$this->messageParser = $this->createMock(MessageParser::class);
@ -117,6 +121,7 @@ class RoomControllerTest extends TestCase {
$this->manager,
$this->roomService,
$this->guestManager,
$this->statusManager,
$this->chatManager,
$this->dispatcher,
$this->messageParser,

Loading…
Cancel
Save