Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ SPDX-FileCopyrightText = "Nextcloud GmbH <https://nextcloud.com/trademarks/>"
SPDX-License-Identifier = "LicenseRef-NextcloudTrademarks"

[[annotations]]
path = ["img/bridge-bot.png", "img/bridge-services/irc.svg", "img/bridge-services/msteams.svg", "img/bridge-services/slack.svg", "img/bridge-services/steam.svg", "img/bridge-services/xmpp.svg", "img/icon-contacts-white.svg", "img/icon-conversation-event-bright.svg", "img/icon-conversation-event-dark.svg", "img/icon-conversation-federation-bright.svg", "img/icon-conversation-federation-dark.svg", "img/icon-conversation-group-bright.svg", "img/icon-conversation-group-dark.svg", "img/icon-conversation-mail-bright.svg", "img/icon-conversation-mail-dark.svg", "img/icon-conversation-password-bright.svg", "img/icon-conversation-password-dark.svg", "img/icon-conversation-phone-bright.svg", "img/icon-conversation-phone-dark.svg", "img/icon-conversation-public-bright.svg", "img/icon-conversation-public-dark.svg", "img/icon-conversation-text-bright.svg", "img/icon-conversation-text-dark.svg", "img/icon-conversation-user-bright.svg", "img/icon-conversation-user-dark.svg", "img/icon-event-white.svg", "img/icon-mail-white.svg", "img/icon-password-white.svg", "img/icon-phone-white.svg", "img/icon-public-white.svg", "img/icon-team-white.svg", "img/icon-text-white.svg", "img/icon-user-white.svg", "img/phone.png", "img/material-icons/*.svg"]
path = ["img/bridge-bot.png", "img/bridge-services/irc.svg", "img/bridge-services/msteams.svg", "img/bridge-services/slack.svg", "img/bridge-services/steam.svg", "img/bridge-services/xmpp.svg", "img/icon-contacts-white.svg", "img/icon-conversation-event-bright.svg", "img/icon-conversation-event-dark.svg", "img/icon-conversation-federation-bright.svg", "img/icon-conversation-federation-dark.svg", "img/icon-conversation-group-bright.svg", "img/icon-conversation-group-dark.svg", "img/icon-conversation-mail-bright.svg", "img/icon-conversation-mail-dark.svg", "img/icon-conversation-password-bright.svg", "img/icon-conversation-password-dark.svg", "img/icon-conversation-phone-bright.svg", "img/icon-conversation-phone-dark.svg", "img/icon-conversation-public-bright.svg", "img/icon-conversation-public-dark.svg", "img/icon-conversation-text-bright.svg", "img/icon-conversation-text-dark.svg", "img/icon-conversation-user-bright.svg", "img/icon-conversation-user-dark.svg", "img/icon-event-white.svg", "img/icon-mail-white.svg", "img/icon-password-white.svg", "img/icon-phone-white.svg", "img/icon-public-white.svg", "img/icon-team-white.svg", "img/icon-text-white.svg", "img/icon-user-white.svg", "img/icon-conversation-voice-room-bright.svg", "img/icon-conversation-voice-room-dark.svg", "img/phone.png", "img/material-icons/*.svg"]
precedence = "aggregate"
SPDX-FileCopyrightText = "2018-2025 Google LLC"
SPDX-License-Identifier = "Apache-2.0"
Expand Down
8 changes: 8 additions & 0 deletions img/icon-conversation-voice-room-bright.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions img/icon-conversation-voice-room-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions lib/RoomPresets/DefaultPreset.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,27 @@
use OCA\Talk\Service\RecordingService;
use OCA\Talk\Webinary;
use OCP\AppFramework\Services\IAppConfig;
use OCP\IL10N;

readonly class DefaultPreset extends APreset {
public const CONFIG_PREFIX_DEFAULT = 'default_';

public function __construct(
protected IAppConfig $appConfig,
protected IL10N $l,
) {
}

#[\Override]
public function getName(): string {
return $this->l->t('Default');
}

#[\Override]
public function getDescription(): string {
return $this->l->t('Send messages, create threads, and start voice and video calls.');
}

#[\Override]
public static function getIdentifier(): string {
return 'default';
Expand Down
2 changes: 1 addition & 1 deletion lib/RoomPresets/VoiceRoom.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public function getName(): string {

#[\Override]
public function getDescription(): string {
return $this->l->t('Voice room');
return $this->l->t('Directly join the call, ideal for catch-ups or spontaneous meetings.');
}

#[\Override]
Expand Down
4 changes: 4 additions & 0 deletions lib/Service/AvatarService.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

use InvalidArgumentException;
use OCA\Talk\Room;
use OCA\Talk\RoomAttributes;
use OCP\Files\IAppData;
use OCP\Files\IFilenameValidator;
use OCP\Files\NotFoundException;
Expand Down Expand Up @@ -273,6 +274,9 @@ private function getAvatarPath(Room $room, bool $darkTheme = false): string {
if ($room->isFederatedConversation()) {
return __DIR__ . '/../../img/icon-conversation-federation-' . $colorTone . '.svg';
}
if (($room->getAttributes() & RoomAttributes::VOICE_ROOM->value) === RoomAttributes::VOICE_ROOM->value) {
return __DIR__ . '/../../img/icon-conversation-voice-room-' . $colorTone . '.svg';
}
if ($room->getType() === Room::TYPE_PUBLIC) {
return __DIR__ . '/../../img/icon-conversation-public-' . $colorTone . '.svg';
}
Expand Down
225 changes: 137 additions & 88 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import { CONVERSATION, PARTICIPANT } from './constants.ts'
import BrowserStorage from './services/BrowserStorage.js'
import { EventBus } from './services/EventBus.ts'
import { leaveConversationSync } from './services/participantsService.js'
import SessionStorage from './services/SessionStorage.js'
import { useActorStore } from './stores/actor.ts'
import { useCallViewStore } from './stores/callView.ts'
import { useSidebarStore } from './stores/sidebar.ts'
Expand Down Expand Up @@ -159,6 +160,14 @@ export default {
currentConversation() {
return this.$store.getters.conversation(this.token)
},

isVoiceRoom() {
return Boolean(this.currentConversation?.attributes & CONVERSATION.ATTRIBUTE.VOICE_ROOM)
},

voiceRoomIdentifier() {
return [this.token, this.isVoiceRoom]
},
},

watch: {
Expand All @@ -176,6 +185,30 @@ export default {
}
},

voiceRoomIdentifier: {
immediate: true,
handler(newValue, oldValue = []) {
const [newToken, newIsVoiceRoom] = newValue
const [oldToken, oldIsVoiceRoom] = oldValue

if (!newIsVoiceRoom && oldIsVoiceRoom && newToken !== oldToken) {
this.callViewStore.setForceCallView(false)
}

if (oldIsVoiceRoom && newToken !== oldToken) {
this.callViewStore.setSelectedVideoPeerId(null)
this.$store.dispatch('leaveCall', {
token: oldToken,
participantIdentifier: this.actorStore.participantIdentifier,
})
}

if (newIsVoiceRoom && newToken) {
this.joinCallAutomatically()
}
},
},

isInCall: {
immediate: true,
handler(value) {
Expand Down Expand Up @@ -264,93 +297,7 @@ export default {
})

EventBus.on('switch-to-conversation', async (params) => {
if (this.isInCall) {
this.callViewStore.setForceCallView(true)

const enableAudio = !BrowserStorage.getItem('audioDisabled_' + this.token)
const enableVideo = !BrowserStorage.getItem('videoDisabled_' + this.token)
const enableVirtualBackground = !!BrowserStorage.getItem('virtualBackgroundEnabled')
const virtualBackgroundType = BrowserStorage.getItem('virtualBackgroundType')
const virtualBackgroundBlurStrength = BrowserStorage.getItem('virtualBackgroundBlurStrength')
const virtualBackgroundUrl = BrowserStorage.getItem('virtualBackgroundUrl')

// Fetch conversation object, if it's not known yet to the client
if (!this.$store.getters.conversation(params.token)) {
await this.fetchSingleConversation(params.token)
}

const conversation = this.$store.getters.conversation(this.token)
const previousParticipants = []
if (conversation.type === CONVERSATION.TYPE.ONE_TO_ONE) {
previousParticipants.push(conversation.name)
}

EventBus.once('joined-conversation', async ({ token }) => {
if (params.token !== token) {
return
}

if (enableAudio) {
BrowserStorage.removeItem('audioDisabled_' + token)
} else {
BrowserStorage.setItem('audioDisabled_' + token, 'true')
}
if (enableVideo) {
BrowserStorage.removeItem('videoDisabled_' + token)
} else {
BrowserStorage.setItem('videoDisabled_' + token, 'true')
}
if (enableVirtualBackground) {
BrowserStorage.setItem('virtualBackgroundEnabled', 'true')
} else {
BrowserStorage.removeItem('virtualBackgroundEnabled')
}
if (virtualBackgroundType) {
BrowserStorage.setItem('virtualBackgroundType', virtualBackgroundType)
} else {
BrowserStorage.removeItem('virtualBackgroundType')
}
if (virtualBackgroundBlurStrength) {
BrowserStorage.setItem('virtualBackgroundBlurStrength', virtualBackgroundBlurStrength)
} else {
BrowserStorage.removeItem('virtualBackgroundBlurStrength')
}
if (virtualBackgroundUrl) {
BrowserStorage.setItem('virtualBackgroundUrl', virtualBackgroundUrl)
} else {
BrowserStorage.removeItem('virtualBackgroundUrl')
}

const conversation = this.$store.getters.conversation(token)

let flags = PARTICIPANT.CALL_FLAG.IN_CALL
if (conversation.permissions & PARTICIPANT.PERMISSIONS.PUBLISH_AUDIO) {
flags |= PARTICIPANT.CALL_FLAG.WITH_AUDIO
}
if (conversation.permissions & PARTICIPANT.PERMISSIONS.PUBLISH_VIDEO) {
flags |= PARTICIPANT.CALL_FLAG.WITH_VIDEO
}

const payload = {
token: params.token,
participantIdentifier: this.actorStore.participantIdentifier,
flags,
silent: true,
recordingConsent: this.recordingConsentGiven,
}

if (conversation.objectType === CONVERSATION.OBJECT_TYPE.EXTENDED) {
payload.silent = false
if (previousParticipants.length) {
payload.silentFor = previousParticipants
}
}

await this.$store.dispatch('joinCall', payload)

this.callViewStore.setForceCallView(false)
})
}
this.joinCallAutomatically()

this.skipLeaveWarning = true
this.$router.push({ name: 'conversation', params: { token: params.token } })
Expand Down Expand Up @@ -436,8 +383,9 @@ export default {
if (from.name === 'conversation' && to.name === 'conversation' && from.params.token === to.params.token) {
// Navigating within the same conversation
beforeRouteChangeListener(to, from, next)
} else if (!this.warnLeaving || this.skipLeaveWarning) {
} else if (!this.warnLeaving || this.skipLeaveWarning || this.isVoiceRoom) {
// Safe to navigate
// Note: voice rooms are intended to be left without confirmation.
beforeRouteChangeListener(to, from, next)
} else {
spawnDialog(ConfirmDialog, {
Expand Down Expand Up @@ -570,6 +518,107 @@ export default {
this.$router.push({ name: 'root' })
}
},

async joinCallAutomatically() {
Comment thread
DorraJaouad marked this conversation as resolved.
if (this.isInCall || this.isVoiceRoom) {
this.callViewStore.setForceCallView(true)

const enableAudio = !BrowserStorage.getItem('audioDisabled_' + this.token)
const enableVideo = !BrowserStorage.getItem('videoDisabled_' + this.token)
const enableVirtualBackground = !!BrowserStorage.getItem('virtualBackgroundEnabled')
const virtualBackgroundType = BrowserStorage.getItem('virtualBackgroundType')
const virtualBackgroundBlurStrength = BrowserStorage.getItem('virtualBackgroundBlurStrength')
const virtualBackgroundUrl = BrowserStorage.getItem('virtualBackgroundUrl')

// Fetch conversation object, if it's not known yet to the client
if (!this.$store.getters.conversation(this.token)) {
await this.fetchSingleConversation(this.token)
}

const conversation = this.$store.getters.conversation(this.token)
const previousParticipants = []
if (conversation.type === CONVERSATION.TYPE.ONE_TO_ONE) {
previousParticipants.push(conversation.name)
}

// Remove previous listener to prevent stacking on rapid token changes
if (this._joinCallHandler) {
EventBus.off('joined-conversation', this._joinCallHandler)
}

this._joinCallHandler = async ({ token }) => {
if (this.token !== token) {
return
}
if (enableAudio) {
BrowserStorage.removeItem('audioDisabled_' + token)
} else {
BrowserStorage.setItem('audioDisabled_' + token, 'true')
}
if (enableVideo) {
BrowserStorage.removeItem('videoDisabled_' + token)
} else {
BrowserStorage.setItem('videoDisabled_' + token, 'true')
}
if (enableVirtualBackground) {
BrowserStorage.setItem('virtualBackgroundEnabled', 'true')
} else {
BrowserStorage.removeItem('virtualBackgroundEnabled')
}
if (virtualBackgroundType) {
BrowserStorage.setItem('virtualBackgroundType', virtualBackgroundType)
} else {
BrowserStorage.removeItem('virtualBackgroundType')
}
if (virtualBackgroundBlurStrength) {
BrowserStorage.setItem('virtualBackgroundBlurStrength', virtualBackgroundBlurStrength)
} else {
BrowserStorage.removeItem('virtualBackgroundBlurStrength')
}
if (virtualBackgroundUrl) {
BrowserStorage.setItem('virtualBackgroundUrl', virtualBackgroundUrl)
} else {
BrowserStorage.removeItem('virtualBackgroundUrl')
}

const conversation = this.$store.getters.conversation(token)

let flags = PARTICIPANT.CALL_FLAG.IN_CALL
if (conversation.permissions & PARTICIPANT.PERMISSIONS.PUBLISH_AUDIO) {
flags |= PARTICIPANT.CALL_FLAG.WITH_AUDIO
}
if (conversation.permissions & PARTICIPANT.PERMISSIONS.PUBLISH_VIDEO) {
flags |= PARTICIPANT.CALL_FLAG.WITH_VIDEO
}

const payload = {
token: this.token,
participantIdentifier: this.actorStore.participantIdentifier,
flags,
silent: true,
recordingConsent: this.recordingConsentGiven,
}

if (conversation.objectType === CONVERSATION.OBJECT_TYPE.EXTENDED) {
payload.silent = false
if (previousParticipants.length) {
payload.silentFor = previousParticipants
}
}

await this.$store.dispatch('joinCall', payload)

this.callViewStore.setForceCallView(false)
}

const currentJoinedToken = SessionStorage.getItem('joined_conversation')
if (currentJoinedToken === this.token) {
this._joinCallHandler({ token: this.token })
} else {
EventBus.once('joined-conversation', this._joinCallHandler)
}
}
},
},
}
</script>
Expand Down
5 changes: 4 additions & 1 deletion src/components/CallView/BottomBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import {
useDocumentFullscreen,
} from '../../composables/useDocumentFullscreen.ts'
import { useGetToken } from '../../composables/useGetToken.ts'
import { CONVERSATION, PARTICIPANT } from '../../constants.ts'
import { ATTENDEE, CONVERSATION, PARTICIPANT } from '../../constants.ts'
import {
getTalkConfig,
showTalkFeatureHint,
Expand Down Expand Up @@ -71,6 +71,8 @@ const isMobile = useIsMobile()
const conversation = computed(() => {
return store.getters.conversation(token.value) || store.getters.dummyConversation
})
const isGuestActor = computed(() => actorStore.actorType === ATTENDEE.ACTOR_TYPE.GUESTS)
const isVoiceRoom = computed(() => Boolean(conversation.value.attributes & CONVERSATION.ATTRIBUTE.VOICE_ROOM))

const supportedReactions = computed(() => getTalkConfig(token.value, 'call', 'supported-reactions') || [])

Expand Down Expand Up @@ -762,6 +764,7 @@ useHotKey('r', toggleHandRaised)
</NcActions>

<CallButton
v-show="!(isVoiceRoom && isGuestActor)"
class="call-button"
:hideText="isSidebar || isMobile"
:isScreensharing="!!localMediaModel.attributes.localScreen" />
Expand Down
Loading
Loading