Skip to content

Commit ee229d2

Browse files
committed
Merge remote-tracking branch 'origin/feat/qall_remake' into feat/remove-old-qall
2 parents 8e8dd53 + bb475ba commit ee229d2

File tree

12 files changed

+754
-24
lines changed

12 files changed

+754
-24
lines changed
Lines changed: 93 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,121 @@
11
<script setup lang="ts">
2-
import { onMounted, onUnmounted, ref, useTemplateRef, watchEffect } from 'vue'
2+
import {
3+
onMounted,
4+
onUnmounted,
5+
ref,
6+
reactive,
7+
useTemplateRef,
8+
watchEffect,
9+
computed
10+
} from 'vue'
311
import type { TrackInfo } from '/@/composables/qall/useLiveKitSDK'
4-
12+
import { useUsersStore } from '/@/store/entities/users'
13+
import { buildUserIconPath } from '/@/lib/apis'
514
const { trackInfo } = defineProps<{
615
trackInfo: TrackInfo
716
}>()
817
const audioElement = useTemplateRef<HTMLMediaElement>('audioElement')
918
const volume = ref(1)
19+
const { findUserByName } = useUsersStore()
20+
const audioContext = new AudioContext()
21+
const gainNode = audioContext.createGain()
22+
gainNode.gain.value = 1
1023
1124
watchEffect(() => {
1225
if (audioElement.value) {
13-
audioElement.value.volume = volume.value
26+
gainNode.gain.value = volume.value
1427
}
1528
})
1629
1730
onMounted(() => {
1831
if (audioElement.value) {
32+
// chromeだと一回要素に入れないと上手く再生してくれない
1933
trackInfo.trackPublication?.track?.attach(audioElement.value)
34+
if (trackInfo.trackPublication?.track?.mediaStream) {
35+
audioContext
36+
.createMediaStreamSource(trackInfo.trackPublication?.track?.mediaStream)
37+
.connect(gainNode)
38+
.connect(audioContext.destination)
39+
audioElement.value.muted = true
40+
}
2041
}
2142
})
2243
2344
onUnmounted(() => {
24-
trackInfo.trackPublication?.track?.detach()
45+
if (audioElement.value) {
46+
trackInfo.trackPublication?.track?.detach(audioElement.value)
47+
audioContext.close()
48+
}
49+
})
50+
const user = computed(() => findUserByName(trackInfo.participantIdentity))
51+
const userIconFileId = computed(() => user.value?.iconFileId ?? '')
52+
const iconStyle = reactive({
53+
container: computed(() => ({
54+
backgroundImage: userIconFileId.value
55+
? `url(${buildUserIconPath(userIconFileId.value)})`
56+
: undefined
57+
}))
2558
})
2659
</script>
2760

2861
<template>
2962
<div>{{ trackInfo.participantIdentity }}</div>
3063
<audio :id="trackInfo.trackPublication?.trackSid" ref="audioElement"></audio>
64+
<input v-model="volume" type="range" min="0" max="3" step="0.01" />
65+
<div :class="$style.UserCard">
66+
<audio
67+
:id="trackInfo.trackPublication?.trackSid"
68+
ref="audioElement"
69+
></audio>
70+
71+
<div :style="iconStyle.container" :class="$style.OuterIcon" />
72+
<div :style="iconStyle.container" :class="$style.InnerIcon" />
73+
74+
<div :class="$style.NameLabel">{{ trackInfo.participantIdentity }}</div>
75+
</div>
3176
<input v-model="volume" type="range" min="0" max="1" step="0.01" />
3277
</template>
78+
<style lang="scss" module>
79+
.UserCard {
80+
height: 108px;
81+
width: 192px;
82+
position: relative;
83+
overflow: hidden;
84+
border-radius: 12px;
85+
}
86+
.InnerIcon {
87+
height: 96px;
88+
width: 96px;
89+
background-size: cover;
90+
border-radius: 50%;
91+
margin: auto;
92+
position: absolute;
93+
top: 0;
94+
right: 0;
95+
bottom: 0;
96+
left: 0;
97+
}
98+
.OuterIcon {
99+
height: 250px;
100+
width: 250px;
101+
position: absolute;
102+
top: 50%;
103+
left: 50%;
104+
transform: translateY(-50%) translateX(-50%);
105+
-webkit-transform: translateY(-50%) translateX(-50%);
106+
overflow: hidden;
107+
filter: blur(40px);
108+
}
109+
.NameLabel {
110+
position: absolute;
111+
left: 8px;
112+
bottom: 8px;
113+
display: flex;
114+
padding: 4px 12px;
115+
align-items: flex-start;
116+
gap: 8px;
117+
border-radius: 8px;
118+
background: rgba(0, 0, 0, 0.5);
119+
color: #fff;
120+
}
121+
</style>

src/components/Main/MainView/QallView/DanmakuContainer.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ onMounted(() => {
125125
.danmakuContainer {
126126
pointer-events: none;
127127
width: 100%;
128-
height: 100%; /* 適切な高さを設定 */
128+
height: 100%;
129129
position: absolute;
130130
bottom: 0;
131131
left: 0;
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
<template>
2+
<div
3+
:class="$style.container"
4+
:data-is-not-messages-show="$boolAttr(!isMessageShow)"
5+
>
6+
<scroll-loading-bar
7+
:class="$style.loadingBar"
8+
:show="isLoading && isMessageShow"
9+
/>
10+
<transition name="fade-bottom" mode="out-in">
11+
<messages-scroller
12+
v-if="isMessageShow"
13+
ref="scrollerEle"
14+
:message-ids="messageIds"
15+
:is-reached-end="isReachedEnd"
16+
:is-reached-latest="isReachedLatest"
17+
:is-loading="isLoading"
18+
:last-loading-direction="lastLoadingDirection"
19+
@request-load-former="onLoadFormerMessagesRequest"
20+
@request-load-latter="onLoadLatterMessagesRequest"
21+
@scroll-passive="handleScroll"
22+
@reset-is-reached-latest="resetIsReachedLatest"
23+
>
24+
<template
25+
#default="{ messageId, onChangeHeight, onEntryMessageLoaded }"
26+
>
27+
<message-element
28+
:class="$style.element"
29+
:message-id="messageId"
30+
:is-archived="isArchived"
31+
@change-height="onChangeHeight"
32+
@entry-message-loaded="onEntryMessageLoaded"
33+
/>
34+
</template>
35+
</messages-scroller>
36+
</transition>
37+
<div :class="$style.uiElement">
38+
<FormButton
39+
label="メッセージを表示"
40+
@click="
41+
() => {
42+
if (isMessageShow) {
43+
isMessageShow = false
44+
toNewMessage('smooth')
45+
} else {
46+
isMessageShow = true
47+
nextTick(() => toNewMessage())
48+
}
49+
}
50+
"
51+
/>
52+
<message-input
53+
:channel-id="channelId"
54+
:typing-users="typingUsers"
55+
:show-to-new-message-button="showToNewMessageButton"
56+
@click-to-new-message-button="toNewMessage"
57+
/>
58+
</div>
59+
</div>
60+
</template>
61+
62+
<script lang="ts" setup>
63+
import MessagesScroller from '/@/components/Main/MainView/MessagesScroller/MessagesScroller.vue'
64+
import MessageInput from '/@/components/Main/MainView/MessageInput/MessageInput.vue'
65+
import ScrollLoadingBar from '/@/components/Main/MainView/ScrollLoadingBar.vue'
66+
import { computed, nextTick, ref, shallowRef } from 'vue'
67+
import type { ChannelId, UserId } from '/@/types/entity-ids'
68+
import useChannelMessageFetcher from '../ChannelView/ChannelViewContent/composables/useChannelMessageFetcher'
69+
import { useChannelsStore } from '/@/store/entities/channels'
70+
import MessageElement from '/@/components/Main/MainView/MessageElement/MessageElement.vue'
71+
import { useSubscriptionStore } from '/@/store/domain/subscription'
72+
import FormButton from '/@/components/UI/FormButton.vue'
73+
74+
const props = defineProps<{
75+
channelId: ChannelId
76+
typingUsers: UserId[]
77+
}>()
78+
79+
const isMessageShow = ref(false)
80+
81+
const scrollerEle = shallowRef<{ $el: HTMLDivElement } | undefined>()
82+
const {
83+
messageIds,
84+
isReachedEnd,
85+
isReachedLatest,
86+
isLoading,
87+
lastLoadingDirection,
88+
onLoadFormerMessagesRequest,
89+
onLoadLatterMessagesRequest
90+
} = useChannelMessageFetcher(scrollerEle, props)
91+
92+
const { channelsMap } = useChannelsStore()
93+
const isArchived = computed(
94+
() => channelsMap.value.get(props.channelId)?.archived ?? false
95+
)
96+
97+
const { unreadChannelsMap } = useSubscriptionStore()
98+
const resetIsReachedLatest = () => {
99+
if (!unreadChannelsMap.value.get(props.channelId)) return
100+
isReachedLatest.value = false
101+
}
102+
103+
const showToNewMessageButton = ref(false)
104+
const toNewMessage = (behavior?: ScrollBehavior) => {
105+
if (!scrollerEle.value) return
106+
showToNewMessageButton.value = false
107+
scrollerEle.value.$el.scrollTo({
108+
top: scrollerEle.value.$el.scrollHeight,
109+
behavior: behavior
110+
})
111+
}
112+
113+
const handleScroll = () => {
114+
if (scrollerEle.value === undefined || isLoading.value) return
115+
const { scrollTop, scrollHeight, clientHeight } = scrollerEle.value.$el
116+
showToNewMessageButton.value = scrollHeight - 2 * clientHeight > scrollTop
117+
if (!isReachedLatest.value) {
118+
showToNewMessageButton.value = true
119+
}
120+
}
121+
</script>
122+
123+
<style lang="scss" module>
124+
.container {
125+
display: flex;
126+
flex: 1 1;
127+
flex-direction: column;
128+
justify-content: end;
129+
position: relative;
130+
width: 100%;
131+
height: 100%;
132+
&[data-is-not-messages-show] {
133+
pointer-events: none;
134+
}
135+
}
136+
137+
.loadingBar {
138+
position: absolute;
139+
top: 0;
140+
left: 0;
141+
right: 0;
142+
height: 12px;
143+
z-index: $z-index-message-loading;
144+
}
145+
146+
.unreadSeparator {
147+
color: $theme-accent-notification-default;
148+
}
149+
150+
.dateSeparator {
151+
@include color-ui-secondary;
152+
}
153+
.element {
154+
margin: 4px 0;
155+
contain: content;
156+
}
157+
.uiElement {
158+
pointer-events: all;
159+
}
160+
</style>

src/components/Main/MainView/QallView/QallView.vue

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
<script setup lang="ts">
2-
import { onMounted, ref } from 'vue'
32
import { useQall } from '/@/composables/qall/useQall'
3+
import UserList from '/@/components/Main/MainView/QallView/UserList.vue'
4+
import { onMounted, ref } from 'vue'
45
import VideoComponent from '/@/components/Main/MainView/QallView/VideoTrack.vue'
56
import AudioComponent from '/@/components/Main/MainView/QallView/AudioTrack.vue'
67
import DanmakuContainer from './DanmakuContainer.vue'
78
import CallControlButton from './CallControlButton.vue'
89
import CallControlButtonSmall from './CallControlButtonSmall.vue'
10+
import ScreenShareComponent from './ScreenShareComponent.vue'
11+
import { LocalTrackPublication } from 'livekit-client'
12+
import ChannelViewContentMain from '../ChannelView/ChannelViewContent/ChannelViewContentMain.vue'
13+
import QallMessageView from './QallMessageView.vue'
914
1015
const {
1116
tracksMap,
17+
screenShareTrackSidMap,
18+
callingChannel,
1219
leaveQall,
1320
addScreenShareTrack,
1421
addCameraTrack,
@@ -69,6 +76,7 @@ const toggleVideo = async () => {
6976
for (const trackInfo of tracksMap.value.values()) {
7077
if (
7178
!trackInfo.isRemote &&
79+
trackInfo.trackPublication instanceof LocalTrackPublication &&
7280
trackInfo.trackPublication?.kind === 'video' &&
7381
!trackInfo.trackPublication.trackName?.includes('screen')
7482
) {
@@ -95,6 +103,7 @@ const toggleScreen = async () => {
95103
for (const trackInfo of tracksMap.value.values()) {
96104
if (
97105
!trackInfo.isRemote &&
106+
trackInfo.trackPublication instanceof LocalTrackPublication &&
98107
trackInfo.trackPublication?.kind === 'video' &&
99108
trackInfo.trackPublication.trackName?.includes('screen')
100109
) {
@@ -140,6 +149,11 @@ const backgroundType = ref<'original' | 'blur' | 'file' | 'screen'>('original')
140149
<template>
141150
<div :class="$style.Block">
142151
<DanmakuContainer />
152+
<QallMessageView
153+
:channel-id="callingChannel"
154+
:typing-users="[]"
155+
:class="$style.channelView"
156+
/>
143157
<h1 :class="$style.Header">Qall View</h1>
144158
{{ backgroundType }}
145159
<button @click="addScreenShareTrack">Add Screen Share Track</button>
@@ -184,17 +198,35 @@ const backgroundType = ref<'original' | 'blur' | 'file' | 'screen'>('original')
184198
>
185199
Add Camera Track
186200
</button>
201+
<UserList />
187202

188203
<div :class="$style.TrackContainer">
189-
<template v-for="(track, index) in tracksMap.values()" :key="index">
204+
<template v-for="[sid, track] in tracksMap.entries()" :key="sid">
190205
<VideoComponent
191-
v-if="track.trackPublication?.kind === 'video'"
206+
v-if="
207+
track.trackPublication?.kind === 'video' &&
208+
!screenShareTrackSidMap.has(sid)
209+
"
192210
:track-info="track"
211+
:class="$style.video"
212+
/>
213+
<ScreenShareComponent
214+
v-else-if="track.trackPublication?.kind === 'video'"
215+
:track-info="track"
216+
:audio-track-info="
217+
tracksMap.get(screenShareTrackSidMap.get(sid) ?? '')
218+
"
193219
:participant-identity="track.participantIdentity"
194220
:class="$style.video"
195221
/>
196222
<AudioComponent
197-
v-else-if="track.trackPublication?.kind === 'audio' && track.isRemote"
223+
v-else-if="
224+
track.trackPublication?.kind === 'audio' &&
225+
track.isRemote &&
226+
!screenShareTrackSidMap
227+
.values()
228+
?.some?.(valueSid => valueSid === sid)
229+
"
198230
:track-info="track"
199231
/>
200232
</template>
@@ -247,6 +279,12 @@ const backgroundType = ref<'original' | 'blur' | 'file' | 'screen'>('original')
247279
.TrackContainer {
248280
height: fit-content;
249281
}
282+
.channelView {
283+
position: absolute;
284+
width: 30%;
285+
right: 0;
286+
bottom: 0;
287+
}
250288
.video {
251289
width: 50%;
252290
height: 50%;

0 commit comments

Comments
 (0)