Skip to content

Commit afe0a27

Browse files
authored
Merge pull request chamilo#6805 from christianbeeznest/chamiloGH-6761
User: Restore global chat - refs chamilo#6761
2 parents d52980f + 43b717f commit afe0a27

File tree

11 files changed

+1303
-65
lines changed

11 files changed

+1303
-65
lines changed

assets/css/scss/_chat.scss

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
.chd {
2+
.chd-fab {
3+
position: fixed; right: 16px; bottom: 88px; z-index: 1100;
4+
width: 52px; height: 52px; border-radius: 9999px; border: none;
5+
display: flex; align-items: center; justify-content: center;
6+
background: #4F46E5; color: #fff; cursor: pointer;
7+
box-shadow: 0 10px 18px rgba(0,0,0,.20), 0 2px 6px rgba(0,0,0,.12);
8+
overflow: visible;
9+
isolation: isolate;
10+
&:hover { background: #4338CA; }
11+
}
12+
.chd-badge {
13+
position: absolute; top: -6px; right: -6px;
14+
min-width: 20px; height: 20px; padding: 0 6px;
15+
border-radius: 9999px; background: #EF4444; color: #fff;
16+
font-size: 12px; line-height: 20px; text-align: center;
17+
box-shadow: 0 0 0 2px #fff;
18+
}
19+
.chd-dock {
20+
position: fixed; right: 16px; bottom: 16px; z-index: 1100;
21+
width: 860px; max-width: calc(100vw - 32px);
22+
height: 540px; max-height: calc(100vh - 32px);
23+
background: #fff; border: 1px solid #e5e7eb; border-radius: 14px;
24+
box-shadow: 0 20px 40px rgba(0,0,0,.18);
25+
display: flex; flex-direction: column; overflow: hidden;
26+
}
27+
.chd-header { display:flex; align-items:center; justify-content:space-between; padding:10px 12px; border-bottom:1px solid #eee; background:#fafafa; flex-shrink:0; }
28+
.chd-title { display:flex; align-items:center; gap:.5rem; font-weight:700; }
29+
.chd-actions { display:flex; align-items:center; gap:.5rem; }
30+
.chd-btn { border:1px solid #e5e7eb; background:#fff; color:#374151; border-radius:10px; padding:6px 10px; cursor:pointer;
31+
&:hover { background:#f9fafb; }
32+
&--ghost { background:transparent; border-color:transparent; }
33+
&--xs { padding:2px 6px; border-radius:8px; }
34+
&--primary { background:#4F46E5; color:#fff; border-color:#4F46E5; &:hover { background:#4338CA; } }
35+
&--danger-outline { border-color:#EF4444; color:#B91C1C; background:#fff; &:hover { background:#FEE2E2; } }
36+
}
37+
.chd-dot { width:10px; height:10px; border-radius:9999px; display:inline-block; margin-right:6px; vertical-align:middle;
38+
&--on{ background:#10B981; }
39+
&--off{ background:#9CA3AF; }
40+
}
41+
.chd-body { flex:1; min-height:0; display:grid; grid-template-columns:300px 1fr; }
42+
.chd-sidebar { border-right:1px solid #eee; display:flex; flex-direction:column; min-width:0; min-height:0;
43+
&__head { padding:8px; display:flex; align-items:center; justify-content:space-between; border-bottom:1px solid #eee; flex-shrink:0; }
44+
}
45+
.chd-contacts { flex:1; min-height:0; overflow-y:auto; padding:8px; overscroll-behavior:contain; }
46+
.chd-legacy a { color:#2563eb; text-decoration:none; } .chd-legacy a:hover{ text-decoration:underline; }
47+
.chd-text--muted { color:#6b7280; font-size:.9rem; }
48+
.chd-center { text-align:center; }
49+
.chd-py-8 { padding:8px 0; }
50+
.chd-py-16 { padding:16px 0; }
51+
.chd-chat { display:flex; flex-direction:column; min-width:0; min-height:0; }
52+
.chd-chat__head { padding:8px; border-bottom:1px solid #eee; flex-shrink:0; background:#fff; position:relative; }
53+
.chd-peer { display:flex; align-items:center; gap:.5rem;
54+
&__meta { min-width:0; }
55+
}
56+
.chd-avatar { width:28px; height:28px; border-radius:9999px; border:1px solid #e5e7eb; object-fit:cover; }
57+
.chd-truncate { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
58+
.chd-unread-dot {
59+
width:10px; height:10px; border-radius:9999px; background:#EF4444; margin-left:auto;
60+
box-shadow:0 0 0 2px #fff;
61+
}
62+
.chd-chat__body { flex:1; min-height:0; overflow-y:auto; background:#fafafa; padding:10px; overscroll-behavior:contain; }
63+
.chd-row { display:flex; margin:8px 0; &--me{justify-content:flex-end;} &--peer{justify-content:flex-start;} }
64+
.chd-bubble { max-width:72%; padding:10px 12px; border-radius:14px; box-shadow:0 1px 2px rgba(0,0,0,.04);
65+
&__content { p{margin:0;} }
66+
&__date { font-size:.72rem; opacity:.8; margin-top:6px; text-align:right; }
67+
}
68+
.chd-row--me { .chd-bubble{ background:#4F46E5; color:#fff; border-top-right-radius:4px; } .chd-bubble__date{ color:#E0E7FF; } }
69+
.chd-row--peer { .chd-bubble{ background:#F3F4F6; color:#111827; border-top-left-radius:4px; } .chd-bubble__date{ color:#6b7280; } }
70+
.chd-composer { position:sticky; bottom:0; border-top:1px solid #eee; padding:8px; background:#fff; flex-shrink:0; }
71+
.chd-input { width:100%; border:1px solid #e5e7eb; border-radius:12px; padding:8px; resize:none; max-height:28vh; overflow-y:auto; font:inherit; }
72+
.chd-composer__actions { display:flex; align-items:center; gap:.5rem; margin-top:6px; }
73+
.chd-hint { font-size:.85rem; color:#6b7280; }
74+
.chd-spacer { flex:1; }
75+
.chd-fab.has-unread::after {
76+
content: "";
77+
position: absolute;
78+
top: -4px;
79+
right: -4px;
80+
width: 14px;
81+
height: 14px;
82+
border-radius: 9999px;
83+
background: #EF4444;
84+
box-shadow: 0 0 0 3px #fff, 0 4px 10px rgba(0,0,0,.25);
85+
z-index: 2;
86+
pointer-events: none;
87+
display: block;
88+
}
89+
.chd-fab.has-unread::after {
90+
content: "";
91+
position: absolute;
92+
top: -2px;
93+
right: -2px;
94+
width: 12px;
95+
height: 12px;
96+
background: #EF4444;
97+
border-radius: 9999px;
98+
box-shadow: 0 0 0 2px #fff;
99+
}
100+
.chd-contacts .chd-contact-row { position: relative; }
101+
.chd-contacts .chd-contact-dot {
102+
position: absolute;
103+
top: 6px;
104+
right: 10px;
105+
width: 10px;
106+
height: 10px;
107+
border-radius: 9999px;
108+
background: #EF4444;
109+
box-shadow: 0 0 0 2px #fff;
110+
pointer-events: none;
111+
}
112+
.chd-peer__meta {
113+
display: flex;
114+
align-items: center;
115+
gap: 8px;
116+
}
117+
.chd-peer__status {
118+
margin-left: 8px;
119+
font-size: 18px;
120+
vertical-align: middle;
121+
}
122+
.chd-presence {
123+
display: inline-block;
124+
width: 10px; height: 10px;
125+
border-radius: 50%;
126+
margin-left: 8px;
127+
background: #9ca3af;
128+
}
129+
.chd-presence.on { background: #22c55e; }
130+
.chd-presence.off { background: #9ca3af; }
131+
.is-online { color: #22c55e; }
132+
.is-offline { color: #9ca3af; }
133+
134+
.chd-contact-row { position:relative; }
135+
.chd-presence-dot { position:absolute; right:8px; top:50%; transform:translateY(-50%); width:10px; height:10px; border-radius:9999px; box-shadow:0 0 0 2px #fff; }
136+
.chd-presence-dot.on { background:#22c55e; }
137+
.chd-presence-dot.off { background:#9ca3af; }
138+
.chd-bubble__meta{display:flex;gap:.5rem;align-items:center;opacity:.8;font-size:.85em}
139+
.chd-bubble__ack{font-variant-numeric:tabular-nums}
140+
}
141+
142+
@media (max-width: 720px) {
143+
.chd .chd-dock { width: 100%; right: 0; left: 0; bottom: 0; border-radius: 10px; }
144+
.chd .chd-body { grid-template-columns: 1fr; }
145+
.chd .chd-sidebar { display: none; }
146+
}

assets/css/scss/index.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,5 +100,6 @@
100100
@include meta.load-css('social');
101101
@include meta.load-css('skill');
102102
@include meta.load-css('survey');
103+
@include meta.load-css('chat');
103104

104105
@include meta.load-css("libs/mediaelementjs/styles");

assets/vue/App.vue

Lines changed: 35 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<ConfirmDialog />
1313

1414
<AccessUrlChooser v-if="!showAccessUrlChosserLayout" />
15+
<DockedChat v-if="showGlobalChat" />
1516
</component>
1617
<Toast position="top-center">
1718
<template #message="slotProps">
@@ -40,7 +41,7 @@
4041
</template>
4142

4243
<script setup>
43-
import { computed, onMounted, onUpdated, provide, ref, watch, watchEffect } from "vue"
44+
import { computed, onMounted, onUpdated, provide, ref, watch, watchEffect, defineAsyncComponent } from "vue"
4445
import { useRoute, useRouter } from "vue-router"
4546
import { DefaultApolloClient } from "@vue/apollo-composable"
4647
import axios from "axios"
@@ -62,7 +63,7 @@ import { useMediaElementLoader } from "./composables/mediaElementLoader"
6263
import apolloClient from "./config/apolloClient"
6364
import { useAccessUrlChooser } from "./composables/accessurl/accessUrlChooser"
6465
import AccessUrlChooser from "./components/accessurl/AccessUrlChooser.vue"
65-
import i18nInstance, { setLocale } from "./i18n"
66+
import { setLocale } from "./i18n"
6667
6768
provide(DefaultApolloClient, apolloClient)
6869
@@ -116,21 +117,9 @@ const layout = computed(() => {
116117
})
117118
118119
const legacyContainer = ref(null)
119-
120-
watch(
121-
() => route.name,
122-
() => {
123-
if (legacyContainer.value) {
124-
legacyContainer.value.innerHTML = ""
125-
}
126-
},
127-
)
128-
120+
watch(() => route.name, () => { if (legacyContainer.value) legacyContainer.value.innerHTML = "" })
129121
watchEffect(() => {
130-
if (!legacyContainer.value) {
131-
return
132-
}
133-
122+
if (!legacyContainer.value) return
134123
const content = document.querySelector("#sectionMainContent")
135124
136125
if (content) {
@@ -173,10 +162,10 @@ axios.interceptors.response.use(
173162
undefined,
174163
(error) =>
175164
new Promise(() => {
176-
if (401 === error.response.status) {
177-
notification.showWarningNotification(error.response.data.error)
178-
} else if (500 === error.response.status) {
179-
notification.showWarningNotification(error.response.data.detail)
165+
if (401 === error.response?.status) {
166+
notification.showWarningNotification(error.response.data?.error)
167+
} else if (500 === error.response?.status) {
168+
notification.showWarningNotification(error.response.data?.detail)
180169
}
181170
182171
throw error
@@ -185,31 +174,16 @@ axios.interceptors.response.use(
185174
186175
platformConfigurationStore.initialize()
187176
188-
// Keep i18n locale synced with your own "useLocale" composable,
189-
// but switch via setLocale(...) to trigger proper remounting & fallbacks.
190-
watch(
191-
() => route.params, // if your appLocale depends on route (keep as-is if needed)
192-
() => {
193-
const { appLocale } = useLocale()
194-
if (appLocale?.value && locale.value !== appLocale.value) {
195-
setLocale(appLocale.value)
196-
}
197-
},
198-
{
199-
immediate: true,
200-
},
201-
)
177+
// i18n sync
178+
watch(() => route.params, () => {
179+
const { appLocale } = useLocale()
180+
if (appLocale?.value && locale.value !== appLocale.value) setLocale(appLocale.value)
181+
}, { immediate: true })
182+
183+
watch(() => securityStore.user?.language, (lang) => {
184+
if (lang && locale.value !== lang) setLocale(lang)
185+
}, { immediate: true })
202186
203-
// Also react to the authenticated user's preferred language (after login)
204-
watch(
205-
() => securityStore.user?.language,
206-
(lang) => {
207-
if (lang && locale.value !== lang) {
208-
setLocale(lang)
209-
}
210-
},
211-
{ immediate: true },
212-
)
213187
214188
onMounted(async () => {
215189
const { loader } = useMediaElementLoader()
@@ -228,4 +202,21 @@ onMounted(async () => {
228202
})
229203
}
230204
})
205+
206+
const DockedChat = defineAsyncComponent(() => import("./components/chat/DockedChat.vue"))
207+
const allowGlobalChat = computed(() => {
208+
if (platformConfigurationStore.isLoading) {
209+
console.log("[CHAT] waiting settings... isLoading=true")
210+
return false
211+
}
212+
const val = platformConfigurationStore.getSetting?.("chat.allow_global_chat")
213+
console.log("[CHAT] getSetting('chat.allow_global_chat') ->", val)
214+
return String(val) === "true"
215+
})
216+
217+
const showGlobalChat = computed(() => {
218+
const visible = securityStore.isAuthenticated && allowGlobalChat.value
219+
console.log("[CHAT] showGlobalChat=", visible, "| isAuthenticated=", securityStore.isAuthenticated, "| allowGlobalChat=", allowGlobalChat.value)
220+
return visible
221+
})
231222
</script>

0 commit comments

Comments
 (0)