Skip to content

Commit

Permalink
Feat: Create filter-plugin architecture for unified search
Browse files Browse the repository at this point in the history
This commit introduces the mechanism for apps out of the call,
to add search filters to the unified search "Places" filter selector.

Signed-off-by: fenn-cs <fenn25.fn@gmail.com>
  • Loading branch information
nfebe committed Feb 12, 2024
1 parent 9192886 commit 33a4fb5
Show file tree
Hide file tree
Showing 6 changed files with 475 additions and 64 deletions.
64 changes: 64 additions & 0 deletions core/src/components/ConversationConstants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
export const CONVERSATION = {
START_CALL: {
EVERYONE: 0,
USERS: 1,
MODERATORS: 2,
},

STATE: {
READ_WRITE: 0,
READ_ONLY: 1,
},

LISTABLE: {
NONE: 0,
USERS: 1,
ALL: 2,
},

TYPE: {
ONE_TO_ONE: 1,
GROUP: 2,
PUBLIC: 3,
CHANGELOG: 4,
ONE_TO_ONE_FORMER: 5,
NOTE_TO_SELF: 6,
},

BREAKOUT_ROOM_MODE: {
NOT_CONFIGURED: 0,
AUTOMATIC: 1,
MANUAL: 2,
FREE: 3,
},

BREAKOUT_ROOM_STATUS: {
// Main room
STOPPED: 0,
STARTED: 1,
// Breakout rooms
STATUS_ASSISTANCE_RESET: 0,
STATUS_ASSISTANCE_REQUESTED: 2,
},

OBJECT_TYPE: {
EMAIL: 'emails',
FILE: 'file',
PHONE: 'phone',
VIDEO_VERIFICATION: 'share:password',
BREAKOUT_ROOM: 'room',
DEFAULT: '',
},
}

export const AVATAR = {
SIZE: {
EXTRA_SMALL: 22,
SMALL: 32,
DEFAULT: 44,
MEDIUM: 64,
LARGE: 128,
EXTRA_LARGE: 180,
FULL: 512,
},
}
266 changes: 266 additions & 0 deletions core/src/components/UnifiedSearch/ConversationIcon.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
<!--
- @copyright Copyright (c) 2019 Joas Schilling <coding@schilljs.com>
-
- @author Joas Schilling <coding@schilljs.com>
-
- @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/>.
-->

<template>
<div class="conversation-icon" :style="{ '--icon-size': `${size}px` }" :class="{ 'offline': offline }">
<div v-if="iconClass" class="avatar icon" :class="iconClass" />
<!-- img is used here instead of NcAvatar to explicitly set key required to avoid glitching in virtual scrolling -->
<img v-else-if="!isOneToOne"
:key="avatarUrl"
:src="avatarUrl"
:width="size"
:height="size"
:alt="item.displayName"
class="avatar icon">
<!-- NcAvatar doesn't fully support props update and works only for 1 user -->
<!-- Using key on NcAvatar forces NcAvatar re-mount and solve the problem, could not really optimal -->
<!-- TODO: Check if props update support in NcAvatar is more performant -->
<NcAvatar v-else
:key="item.token"
:size="size"
:user="item.name"
:disable-menu="disableMenu"
:display-name="item.displayName"
:preloaded-user-status="preloadedUserStatus"
:show-user-status="!hideUserStatus"
:show-user-status-compact="!showUserOnlineStatus"
:menu-container="menuContainer"
class="conversation-icon__avatar" />
<div v-if="showCall" class="overlap-icon">
<VideoIcon :size="20" :fill-color="'#E9322D'" />
<span class="hidden-visually">{{ t('spreed', 'Call in progress') }}</span>
</div>
<div v-else-if="showFavorite" class="overlap-icon">
<Star :size="20" :fill-color="'#FFCC00'" />
<span class="hidden-visually">{{ t('spreed', 'Favorite') }}</span>
</div>
</div>
</template>

<script>
import Star from 'vue-material-design-icons/Star.vue'
import VideoIcon from 'vue-material-design-icons/Video.vue'

import { getCapabilities } from '@nextcloud/capabilities'
import { generateOcsUrl } from '@nextcloud/router'

import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'

import { AVATAR, CONVERSATION } from '../ConversationConstants.js'

const supportsAvatar = getCapabilities()?.spreed?.features?.includes('avatar')

export default {
name: 'ConversationIcon',

components: {
NcAvatar,
Star,
VideoIcon,
},

props: {
/**
* Allow to hide the favorite icon, e.g. on mentions
*/
hideFavorite: {
type: Boolean,
default: true,
},

hideCall: {
type: Boolean,
default: true,
},

disableMenu: {
type: Boolean,
default: true,
},

hideUserStatus: {
type: Boolean,
default: false,
},

showUserOnlineStatus: {
type: Boolean,
default: false,
},

item: {
type: Object,
default() {
return {
objectType: '',
type: 0,
displayName: '',
isFavorite: false,
}
},
},

/**
* Reduces the opacity of the icon if true
*/
offline: {
type: Boolean,
default: false,
},

size: {
type: Number,
default: AVATAR.SIZE.DEFAULT,
},
},

computed: {
showCall() {
return !this.hideCall && this.item.hasCall
},

showFavorite() {
return !this.hideFavorite && this.item.isFavorite
},

preloadedUserStatus() {
if (!this.hideUserStatus && Object.prototype.hasOwnProperty.call(this.item, 'statusMessage')) {
// We preloaded the status
return {
status: this.item.status || null,
message: this.item.statusMessage || null,
icon: this.item.statusIcon || null,
}
}
return undefined
},

menuContainer() {
// The store may not be defined in the RoomSelector if used from
// the Collaboration menu outside Talk.
return this.$store?.getters.getMainContainerSelector()
},

iconClass() {
if (this.item.isDummyConversation) {
// Prevent a 404 when trying to load an avatar before the conversation data is actually loaded
return 'icon-contacts'
}

if (!supportsAvatar) {
if (this.item.objectType === CONVERSATION.OBJECT_TYPE.FILE) {
return 'icon-file'
} else if (this.item.objectType === CONVERSATION.OBJECT_TYPE.VIDEO_VERIFICATION) {
return 'icon-password'
} else if (this.item.objectType === CONVERSATION.OBJECT_TYPE.EMAIL) {
return 'icon-mail'
} else if (this.item.objectType === CONVERSATION.OBJECT_TYPE.PHONE) {
return 'icon-phone'
} else if (this.item.type === CONVERSATION.TYPE.CHANGELOG) {
return 'icon-changelog'
} else if (this.item.type === CONVERSATION.TYPE.ONE_TO_ONE_FORMER) {
return 'icon-user'
} else if (this.item.type === CONVERSATION.TYPE.GROUP) {
return 'icon-contacts'
} else if (this.item.type === CONVERSATION.TYPE.PUBLIC) {
return 'icon-public'
}
return undefined
}

if (this.item.token) {
// Existing conversations use the /avatar endpoint… Always!
return undefined
}

if (this.item.type === CONVERSATION.TYPE.GROUP) {
// Group icon for group conversation suggestions
return 'icon-contacts'
}

if (this.item.type === CONVERSATION.TYPE.PUBLIC) {
// Public icon for new conversation dialog
return 'icon-public'
}

// Fall-through for other conversation suggestions to user-avatar handling
return undefined
},

isOneToOne() {
return this.item.type === CONVERSATION.TYPE.ONE_TO_ONE
},
checkIfDarkTheme() {
// Nextcloud uses --background-invert-if-dark for dark theme filters in CSS
// Values:
// - 'invert(100%)' for dark theme
// - 'no' for light theme
return window.getComputedStyle(document.body).getPropertyValue('--background-invert-if-dark') === 'invert(100%)'
},
avatarUrl() {
if (!supportsAvatar) {
return undefined
}

const avatarEndpoint = 'apps/spreed/api/v1/room/{token}/avatar' + (this.checkIfDarkTheme ? '/dark' : '')

return generateOcsUrl(avatarEndpoint + '?v={avatarVersion}', {
token: this.item.token,
avatarVersion: this.item.avatarVersion,
})
},
},
}
</script>

<style lang="scss" scoped>
.conversation-icon {
width: var(--icon-size);
height: var(--icon-size);
position: relative;

.avatar.icon {
display: block;
width: var(--icon-size);
height: var(--icon-size);
line-height: var(--icon-size);
background-size: calc(var(--icon-size) / 2);
background-color: var(--color-background-darker);

&.icon-changelog {
background-size: cover !important;
}
}

.overlap-icon {
position: absolute;
top: 0;
left: calc(var(--icon-size) - 12px);
line-height: 100%;
display: inline-block;
vertical-align: middle;
}
}

.offline {
opacity: .4;
}
</style>
11 changes: 11 additions & 0 deletions core/src/store/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Vue from 'vue';
import Vuex from 'vuex';
import search from './unified-search-external-filters';

Vue.use(Vuex);

export default new Vuex.Store({
modules: {
search,
},
});
42 changes: 42 additions & 0 deletions core/src/store/unified-search-external-filters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* @copyright Copyright (c) 2021 Fon E. Noel NFEBE <me@nfebe.com>
*
* @author Fon E. Noel NFEBE <me@nfebe.com>
*
* @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 state = {
externalFilters: [],
}

const mutations = {
registerExternalFilter(state, { id, label, callback, icon }) {
state.externalFilters.push({ id, name: label, callback, icon, isPluginFilter: true })
},
}

const actions = {
registerExternalFilter({ commit }, { id, label, callback, icon }) {
commit('registerExternalFilter', { id, label, callback, icon })
},
}

export default {
state,
mutations,
actions,
}
Loading

0 comments on commit 33a4fb5

Please sign in to comment.