added room actions: leave and delete, and improved connection error handling
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "frangipane-client",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"backendVersion": "1.0.2",
|
||||
"version": "1.0.0",
|
||||
"backendVersion": "1.0.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "frangipane",
|
||||
"version": "0.1.0",
|
||||
"version": "1.0.0",
|
||||
"identifier": "com.strawberries.frangipane",
|
||||
"build": {
|
||||
"beforeDevCommand": "yarn dev",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { apiFetch } from './client'
|
||||
import type { Room, RoomInvite } from '../types'
|
||||
import { UserProfile, type Room, type RoomInvite } from '../types'
|
||||
|
||||
export function fetchRooms() {
|
||||
return apiFetch<Room[]>(`/rooms`)
|
||||
@@ -40,3 +40,26 @@ export function declineRoomInvite(senderUuid: string, roomUuid: string) {
|
||||
body: JSON.stringify({ sender_uuid: senderUuid, room_uuid: roomUuid }),
|
||||
})
|
||||
}
|
||||
|
||||
export function leaveRoom(roomUuid: string) {
|
||||
return apiFetch<void>(`/rooms/${roomUuid}/leave`, {
|
||||
method: 'POST'
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteRoom(roomUuid: string) {
|
||||
return apiFetch<void>(`/rooms/${roomUuid}/delete`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
}
|
||||
|
||||
export function transferOwnership(roomUuid: string, newOwnerUuid: string) {
|
||||
return apiFetch<void>('/rooms/transfer-ownership', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ room_uuid: roomUuid, new_owner_uuid: newOwnerUuid }),
|
||||
})
|
||||
}
|
||||
|
||||
export function listMembers(roomUuid: string) {
|
||||
return apiFetch<UserProfile[]>(`/rooms/${roomUuid}/members`)
|
||||
}
|
||||
|
||||
10
src/base.css
10
src/base.css
@@ -24,6 +24,7 @@ body,
|
||||
--muted: #9aa0aa;
|
||||
--accent: #f27aa3;
|
||||
--accent-hover: #ff91b3;
|
||||
--accent-second: #96CDFB;
|
||||
--border: #2a2f3b;
|
||||
--error: #ff5050;
|
||||
--radius: 8px;
|
||||
@@ -174,6 +175,15 @@ i:hover {
|
||||
margin: 30px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
border: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
i:hover {
|
||||
color: var(--text);
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
<template>
|
||||
<InvitePeopleModal v-if="showInviteModal && isSocketConnected" :room_uuid="props.uuid"
|
||||
@close="showInviteModal = false" />
|
||||
@close="showInviteModal = false" @room-changed="handleRoomChanged" />
|
||||
|
||||
<RoomDetailsModal v-if="showDetailsModal && isSocketConnected" :roomUuid="props.uuid"
|
||||
:roomName="currentRoom?.name || 'Unknown room'" :isGlobal="currentRoom?.global || false"
|
||||
@close="showDetailsModal = false" @room-changed="handleRoomChanged" />
|
||||
|
||||
<div v-if="uuid === 'none'" class="no-room">
|
||||
<div class="empty-state">
|
||||
@@ -10,15 +14,30 @@
|
||||
</div>
|
||||
|
||||
<div v-else class="chat-container">
|
||||
<h2 class="room-name">{{ currentRoom?.name }}</h2>
|
||||
<button class="room-name" @click="showDetailsModal = true">{{ currentRoom?.name }}</button>
|
||||
|
||||
<div class="messages-container" ref="messageListRef" @scroll="handleScroll">
|
||||
<MessageList v-if="messages.length > 0 || isSocketConnected" :messages="messages" />
|
||||
</div>
|
||||
|
||||
<p v-if="messages.length === 0" class="wait-msg">{{ $t('chat-connecting') }}</p>
|
||||
<div v-if="messages.length == 0" class="center-status-container">
|
||||
<p v-if="isInitialLoad" class="wait-msg">{{ $t('chat-connecting') }}</p>
|
||||
|
||||
<div class="input-container">
|
||||
<div v-else-if="connectionError" class="wait-msg">
|
||||
<p>{{ $t('chat-connecting-failed') }}</p>
|
||||
|
||||
<button class="retry-btn" @click="retryConnection">
|
||||
<i class="fa-solid fa-rotate-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!connectionError && isSocketConnected" class="empty-room-state">
|
||||
<i class="fa-regular fa-paper-plane"></i>
|
||||
<p>{{ $t('chat-no-messages') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isSocketConnected" class="input-container">
|
||||
<button v-if="isOwner && !currentRoom?.global" class="invite-btn" @click="showInviteModal = true"
|
||||
:title="$t('chat-invite-title')">
|
||||
<i class="fa-solid fa-users"></i>
|
||||
@@ -26,9 +45,6 @@
|
||||
|
||||
<div v-if="connectionError" class="connection-error">
|
||||
<p>{{ connectionError }}</p>
|
||||
<button class="retry-btn" @click="retryConnection">
|
||||
<i class="fa-solid fa-rotate-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<MessageInput v-if="isSocketConnected" ref="messageInputRef" @send="onSend" />
|
||||
@@ -45,6 +61,7 @@ import { apiFetch } from '../api/client';
|
||||
import MessageList from "./MessageList.vue";
|
||||
import MessageInput from "./MessageInput.vue";
|
||||
import InvitePeopleModal from './InvitePeopleModal.vue';
|
||||
import RoomDetailsModal from "./RoomDetailsModal.vue";
|
||||
import WebSocket from '@tauri-apps/plugin-websocket';
|
||||
import { getAuthData } from "../store.ts";
|
||||
import { fetchRoomInfo } from "../api/rooms.ts";
|
||||
@@ -53,10 +70,11 @@ import { useFluent } from 'fluent-vue';
|
||||
const { $t } = useFluent();
|
||||
|
||||
const emit = defineEmits<{
|
||||
// (e: 'send', content: string): void
|
||||
(e: 'notification', roomUuid: string): void
|
||||
(e: 'room-action'): void
|
||||
}>();
|
||||
|
||||
|
||||
const props = defineProps<{ uuid: string }>();
|
||||
|
||||
// UI State
|
||||
@@ -71,6 +89,8 @@ const connectionError = ref<string | null>(null);
|
||||
const isLoadingMore = ref(false);
|
||||
const hasMore = ref(true);
|
||||
const showInviteModal = ref(false);
|
||||
const showDetailsModal = ref(false);
|
||||
const isInitialLoad = ref(false);
|
||||
|
||||
// WebSocket State
|
||||
let socket: WebSocket | null = null;
|
||||
@@ -82,6 +102,11 @@ const isOwner = computed(() => {
|
||||
return currentUser.value.uuid === currentRoom.value.owner_uuid;
|
||||
});
|
||||
|
||||
const handleRoomChanged = () => {
|
||||
showDetailsModal.value = false;
|
||||
emit('room-action');
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await connectGlobalWebSocket();
|
||||
|
||||
@@ -109,12 +134,15 @@ async function retryConnection() {
|
||||
}
|
||||
|
||||
async function loadRoomData() {
|
||||
isInitialLoad.value = true;
|
||||
messages.value = [];
|
||||
currentRoom.value = null;
|
||||
hasMore.value = true;
|
||||
connectionError.value = null;
|
||||
|
||||
if (props.uuid === 'none') return;
|
||||
if (props.uuid === 'none') {
|
||||
isInitialLoad.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const [msgs, roomInfo, auth] = await Promise.all([
|
||||
@@ -132,6 +160,8 @@ async function loadRoomData() {
|
||||
scrollToBottom();
|
||||
} catch (err) {
|
||||
console.error("Room data load failed:", err);
|
||||
} finally {
|
||||
isInitialLoad.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +207,7 @@ async function connectGlobalWebSocket() {
|
||||
} catch (err) {
|
||||
console.error("WS Connect failed:", err);
|
||||
isSocketConnected.value = false;
|
||||
connectionError.value = "Live chat disconnected.";
|
||||
connectionError.value = $t('shared-error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,6 +344,14 @@ async function onSend(content: string) {
|
||||
}
|
||||
|
||||
.room-name {
|
||||
border-radius: var(--radius);
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
|
||||
font-size: 1.6rem;
|
||||
font-weight: bold;
|
||||
margin: 15px 0;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -371,4 +409,44 @@ async function onSend(content: string) {
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.center-status-container {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wait-msg {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1.6rem;
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.empty-room-state {
|
||||
color: var(--muted);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.empty-room-state i {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.empty-room-state p {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
97
src/components/ConfirmModal.vue
Normal file
97
src/components/ConfirmModal.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<div class="backdrop" @click.self="emit('no')">
|
||||
<div class="modal">
|
||||
<h2>{{ title }}</h2>
|
||||
<p v-if="message">{{ message }}</p>
|
||||
|
||||
<div class="actions">
|
||||
<button @click="emit('no')" class="secondary">
|
||||
{{ cancelLabel || $t('shared-cancel') || 'Cancel' }}
|
||||
</button>
|
||||
<button @click="emit('yes')" :class="['btn', confirmButtonClass]">
|
||||
{{ confirmLabel || $t('shared-confirm') || 'Confirm' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
title: string;
|
||||
message?: string;
|
||||
confirmLabel?: string;
|
||||
cancelLabel?: string;
|
||||
confirmButtonClass?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['yes', 'no']);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 1.5rem;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
}
|
||||
|
||||
.modal h2 {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.modal p {
|
||||
margin: 0;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
cursor: pointer;
|
||||
padding: 8px 16px;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--accent);
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.secondary {
|
||||
cursor: pointer;
|
||||
padding: 8px 16px;
|
||||
border-radius: var(--radius);
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
border-color: var(--error);
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background-color: var(--error);
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
@@ -2,7 +2,7 @@
|
||||
<ul>
|
||||
<li v-for="(m, i) in messages" :key="i" class="message" :class="{ 'is-me': m.sender_uuid === currentUserUuid }">
|
||||
<div class="sender-info">
|
||||
<img :src="getAvatarUrl(m.sender_uuid)" @error="handleAvatarError" class="sender-avatar" />
|
||||
<img :src="getAvatarUrl(m.sender_uuid)" @error="handleAvatarError" class="avatar" />
|
||||
<div class="sender">{{ m.sender }}</div>
|
||||
<span class="timestamp">{{ m.sent_at }}</span>
|
||||
</div>
|
||||
@@ -74,15 +74,6 @@ ul {
|
||||
padding: 5px 18px;
|
||||
}
|
||||
|
||||
.sender-avatar {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
border: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.message.is-me {
|
||||
align-self: flex-end;
|
||||
align-items: flex-end;
|
||||
|
||||
280
src/components/RoomDetailsModal.vue
Normal file
280
src/components/RoomDetailsModal.vue
Normal file
@@ -0,0 +1,280 @@
|
||||
<template>
|
||||
<div class="backdrop" @click.self="emit('close')">
|
||||
<div class="modal">
|
||||
<h2>{{ roomName }}</h2>
|
||||
<h3 v-if="!isGlobal">Members:</h3>
|
||||
|
||||
<ul v-if="!isGlobal" class="member-list">
|
||||
<li v-for="user in users" :key="user.uuid || user.username" class="member-item">
|
||||
<img :src="getAvatarUrl(user.uuid)" @error="handleAvatarError" class="avatar" alt="avatar" />
|
||||
<span class="member-name">{{ user.username }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p v-else class="global-tag">
|
||||
<i class="fa-solid fa-earth"></i>
|
||||
{{ $t('chat-room-global') }}
|
||||
</p>
|
||||
|
||||
<div class="buttons">
|
||||
<button v-if="!isGlobal && !isOwner" class="btn" @click="requestLeave" :disabled="isLoading">
|
||||
{{ $t('chat-room-actions-leave') }}
|
||||
</button>
|
||||
|
||||
<button v-if="isOwner" class="btn delete-btn" @click="requestDelete" :disabled="isLoading">
|
||||
{{ $t('chat-room-actions-delete') }}
|
||||
</button>
|
||||
|
||||
<!-- <button class="btn ownership-btn">{{ $t('chat-room-actions-ownership') }}</button> -->
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button @click="emit('close')" class="secondary">
|
||||
{{ $t('shared-close') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConfirmModal v-if="modalState.visible" :title="modalState.title" :message="modalState.message"
|
||||
:confirm-label="modalState.confirmLabel" :confirm-button-class="modalState.isDanger ? 'btn-danger' : ''"
|
||||
@yes="handleConfirmAction" @no="closeConfirmModal" />
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, reactive } from 'vue';
|
||||
import { UserProfile } from '../types';
|
||||
import { deleteRoom, leaveRoom, listMembers } from '../api/rooms';
|
||||
import { getAuthData, getAvatarUrl } from '../store.ts';
|
||||
import defaultAvatar from '../assets/default-avatar.png';
|
||||
import ConfirmModal from './ConfirmModal.vue';
|
||||
import { useFluent } from 'fluent-vue';
|
||||
|
||||
const { $t } = useFluent()
|
||||
|
||||
const props = defineProps<{ roomUuid: string, roomName: string, isGlobal: boolean }>()
|
||||
const emit = defineEmits(['close', 'room-changed']);
|
||||
|
||||
const users = ref<UserProfile[]>([]);
|
||||
const isLoading = ref(false);
|
||||
const currentUserUuid = ref<string>('');
|
||||
|
||||
type ActionType = 'leave' | 'delete' | null;
|
||||
|
||||
const modalState = reactive({
|
||||
visible: false,
|
||||
type: null as ActionType,
|
||||
title: '',
|
||||
message: '',
|
||||
confirmLabel: '',
|
||||
isDanger: false
|
||||
});
|
||||
|
||||
const requestLeave = () => {
|
||||
modalState.type = 'leave';
|
||||
modalState.title = $t('chat-room-actions-leave');
|
||||
modalState.message = $t('chat-room-actions-leave-confirm');
|
||||
modalState.confirmLabel = $t('shared-leave');
|
||||
modalState.isDanger = false;
|
||||
modalState.visible = true;
|
||||
};
|
||||
|
||||
const requestDelete = () => {
|
||||
modalState.type = 'delete';
|
||||
modalState.title = $t('chat-room-actions-delete');
|
||||
modalState.message = $t('chat-room-actions-delete-confirm');
|
||||
modalState.confirmLabel = $t('shared-delete');
|
||||
modalState.isDanger = true;
|
||||
modalState.visible = true;
|
||||
};
|
||||
|
||||
const closeConfirmModal = () => {
|
||||
modalState.visible = false;
|
||||
modalState.type = null;
|
||||
};
|
||||
|
||||
const handleConfirmAction = async () => {
|
||||
const actionType = modalState.type;
|
||||
|
||||
closeConfirmModal();
|
||||
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
if (actionType === 'leave') {
|
||||
await leaveRoom(props.roomUuid);
|
||||
emit('room-changed');
|
||||
emit('close');
|
||||
} else if (actionType === 'delete') {
|
||||
await deleteRoom(props.roomUuid);
|
||||
emit('room-changed');
|
||||
emit('close');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to ${actionType} room:`, error);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const isOwner = computed(() => {
|
||||
return true; // Logic placeholder
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
const auth = await getAuthData();
|
||||
currentUserUuid.value = auth.user?.uuid || 'undefined';
|
||||
|
||||
if (!props.isGlobal) {
|
||||
users.value = await listMembers(props.roomUuid);
|
||||
}
|
||||
});
|
||||
|
||||
const handleAvatarError = (event: Event) => {
|
||||
const img = event.target as HTMLImageElement;
|
||||
img.src = defaultAvatar;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 500;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 1.5rem;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.secondary {
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
padding: 8px 16px;
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.member-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
|
||||
gap: 1rem;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.member-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.member-name {
|
||||
font-size: 0.85rem;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.global-tag {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.6rem;
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 16px;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text);
|
||||
background-color: transparent;
|
||||
border-radius: var(--radius);
|
||||
width: fit-content;
|
||||
border: 1px solid var(--accent);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
color: var(--accent)
|
||||
}
|
||||
|
||||
.btn:hover i {
|
||||
color: var(--accent)
|
||||
}
|
||||
|
||||
.btn i {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
border: 1px solid var(--error);
|
||||
}
|
||||
|
||||
.delete-btn:hover {
|
||||
color: var(--error)
|
||||
}
|
||||
|
||||
.delete-btn:hover i {
|
||||
color: var(--error)
|
||||
}
|
||||
|
||||
.ownership-btn {
|
||||
border: 1px solid var(--accent-second);
|
||||
}
|
||||
|
||||
.ownership-btn:hover {
|
||||
color: var(--accent-second)
|
||||
}
|
||||
|
||||
.ownership-btn:hover i {
|
||||
color: var(--accent-second)
|
||||
}
|
||||
</style>
|
||||
@@ -11,14 +11,18 @@
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="wait-container" v-if="!rooms || rooms.length === 0">
|
||||
<div class="wait-container" v-if="isLoading">
|
||||
<p class="wait-msg">{{ $t('chat-room-list-connecting') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="wait-container" v-else-if="!rooms || rooms.length === 0">
|
||||
<p class="wait-msg">{{ $t('chat-room-list-empty') }}</p>
|
||||
<button class="retry-btn" @click="refreshRooms()">
|
||||
<i class="fa-solid fa-rotate-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="scroll-area">
|
||||
<div class="scroll-area" v-else>
|
||||
<router-link v-for="room in rooms" :key="room.uuid" :to="`/rooms/${room.uuid}`" class="btn room-item"
|
||||
:class="{ active: route.params.uuid === room.uuid }" @click="emit('select-room')">
|
||||
<div class="room-content-wrapper">
|
||||
@@ -48,21 +52,27 @@ const route = useRoute();
|
||||
const showCreate = ref(false);
|
||||
const rooms = ref<Room[]>([]);
|
||||
const unreadCounts = ref<Record<string, number>>({});
|
||||
const isLoading = ref(true);
|
||||
|
||||
async function refreshRooms() {
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
const fetchedRooms = await fetchRooms();
|
||||
rooms.value = fetchedRooms;
|
||||
|
||||
fetchedRooms.forEach(room => {
|
||||
console.log(`Unread count for room ${room.name}: ${room.unread_count}`);
|
||||
|
||||
// If the room isn't the currently active one, store the count
|
||||
if (room.unread_count && route.params.uuid !== room.uuid) {
|
||||
unreadCounts.value[room.uuid] = room.unread_count;
|
||||
} else {
|
||||
unreadCounts.value[room.uuid] = 0;
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to refresh rooms", error);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const incrementUnread = (roomUuid: string) => {
|
||||
@@ -72,7 +82,7 @@ const incrementUnread = (roomUuid: string) => {
|
||||
unreadCounts.value[roomUuid] = current + 1;
|
||||
};
|
||||
|
||||
defineExpose({ incrementUnread });
|
||||
defineExpose({ incrementUnread, refreshRooms });
|
||||
|
||||
watch(
|
||||
() => route.params.uuid,
|
||||
@@ -85,7 +95,6 @@ watch(
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
// rooms.value = await fetchRooms();
|
||||
await refreshRooms()
|
||||
});
|
||||
</script>
|
||||
@@ -111,6 +120,7 @@ onMounted(async () => {
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wait-msg {
|
||||
@@ -162,7 +172,6 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
.rooms-header h2 {
|
||||
/* font-size: 1rem; */
|
||||
margin: 0;
|
||||
margin-left: 45px;
|
||||
}
|
||||
@@ -189,7 +198,6 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
.room-item.active {
|
||||
/* border: 1px solid var(--border); */
|
||||
background: var(--panel-accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ auth-error-email-invalid = Please enter a valid email address
|
||||
|
||||
## Chat page
|
||||
chat-no-room = Select a room to start talking
|
||||
chat-no-messages = No messages yet. Say hi!
|
||||
chat-input-placeholder = type a message
|
||||
chat-invite-title = Invite People
|
||||
chat-invite-receiver = Receiver username
|
||||
@@ -32,8 +33,16 @@ chat-invite-friend-too = Also send a friend request
|
||||
chat-invite-send = Send
|
||||
chat-invite-username-placeholder = username
|
||||
chat-room-list-title = Rooms
|
||||
chat-room-list-empty = No rooms found
|
||||
chat-room-list-connecting = Connecting...
|
||||
chat-room-owner = by {$owner}
|
||||
chat-room-global = Global room
|
||||
chat-room-members = Members
|
||||
chat-room-actions-leave = Leave Room
|
||||
chat-room-actions-leave-confirm = Are you sure you want to leave this room?
|
||||
chat-room-actions-delete = Delete Room
|
||||
chat-room-actions-delete-confirm = Are you sure you want to delete this room? This cannot be undone.
|
||||
chat-room-actions-ownership = Transfer Ownership
|
||||
chat-create-title = Create room
|
||||
chat-create-name = Room name
|
||||
chat-create-name-placeholder = room name
|
||||
@@ -95,6 +104,9 @@ warning-wrongversion-dismiss = I know what I'm doing
|
||||
|
||||
## Shared
|
||||
shared-cancel = Cancel
|
||||
shared-close = Close
|
||||
shared-error = An error occurred
|
||||
shared-save = Save
|
||||
shared-updating = Updating
|
||||
shared-delete = Delete
|
||||
shared-leave = Leave
|
||||
|
||||
@@ -25,8 +25,10 @@ auth-error-email-invalid = Veuillez entrer une adresse email valide
|
||||
|
||||
## Chat page
|
||||
chat-no-room = Sélectionnez un salon pour commencer à discuter
|
||||
chat-no-messages = Pas encore de messages. Dites bonjour !
|
||||
chat-input-placeholder = tapez un message
|
||||
chat-invite-title = Inviter des gens
|
||||
chat-room-list-empty = Aucun salon trouvé
|
||||
chat-invite-receiver = Nom du destinataire
|
||||
chat-invite-friend-too = Envoyer aussi une demande d'ami
|
||||
chat-invite-send = Envoyer
|
||||
@@ -34,6 +36,13 @@ chat-invite-username-placeholder = nom d'utilisateur
|
||||
chat-room-list-title = Salons
|
||||
chat-room-list-connecting = Connexion...
|
||||
chat-room-owner = par {$owner}
|
||||
chat-room-global = Salon global
|
||||
chat-room-members = Membres
|
||||
chat-room-actions-leave = Quitter le Salon
|
||||
chat-room-actions-leave-confirm = Voulez-vous vraiment quitter ce salon?
|
||||
chat-room-actions-delete = Supprimer le Salon
|
||||
chat-room-actions-delete-confirm = Voulez-vous vraiment supprimer ce salon? C'est irréversible.
|
||||
chat-room-actions-ownership = Transférer la Propriété
|
||||
chat-create-title = Créer un salon
|
||||
chat-create-name = Nom du salon
|
||||
chat-create-name-placeholder = nom du salon
|
||||
@@ -93,6 +102,9 @@ warning-wrongversion-dismiss = Je sais ce que je fais
|
||||
|
||||
## Shared
|
||||
shared-cancel = Annuler
|
||||
shared-close = Fermer
|
||||
shared-error = Une erreur est survenue
|
||||
shared-save = Enregistrer
|
||||
shared-updating = Mise à jour...
|
||||
shared-delete = Supprimer
|
||||
shared-leave = Quitter
|
||||
|
||||
@@ -28,9 +28,9 @@ async function init() {
|
||||
|
||||
init()
|
||||
|
||||
// export const API = 'http://127.0.0.1:8080'
|
||||
export const API = 'http://192.168.1.183:8080'
|
||||
export const API = 'http://127.0.0.1:8080'
|
||||
// export const API = 'http://192.168.1.183:8080'
|
||||
// export const API = 'https://alatreon.org/frangipane'
|
||||
// export const API_WS = 'ws://127.0.0.1:8080/ws'
|
||||
export const API_WS = 'ws://192.168.1.183:8080/ws'
|
||||
export const API_WS = 'ws://127.0.0.1:8080/ws'
|
||||
// export const API_WS = 'ws://192.168.1.183:8080/ws'
|
||||
// export const API_WS = 'wss://alatreon.org/frangipane/ws'
|
||||
|
||||
@@ -13,27 +13,39 @@
|
||||
<div v-if="isSidebarOpen" class="sidebar-overlay" @click="isSidebarOpen = false"></div>
|
||||
|
||||
<main class="chat-window-container" :class="{ 'sidebar-is-open': isSidebarOpen }">
|
||||
<ChatWindow :uuid="uuid" @notification="handleNotification" />
|
||||
<ChatWindow :uuid="safeUuid" @notification="handleNotification" @room-action="handleRoomAction" />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { ref, computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import RoomList from "../components/RoomList.vue";
|
||||
import ChatWindow from "../components/ChatWindow.vue";
|
||||
|
||||
defineProps<{ uuid: string }>();
|
||||
const isSidebarOpen = ref(true);
|
||||
const props = defineProps<{ uuid?: string }>();
|
||||
const router = useRouter();
|
||||
|
||||
const isSidebarOpen = ref(true);
|
||||
const roomListRef = ref<InstanceType<typeof RoomList> | null>(null);
|
||||
|
||||
const safeUuid = computed(() => props.uuid || 'none');
|
||||
|
||||
const handleRoomSelection = () => {
|
||||
if (window.innerWidth <= 720) {
|
||||
isSidebarOpen.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleRoomAction = async () => {
|
||||
if (roomListRef.value) {
|
||||
await roomListRef.value.refreshRooms();
|
||||
}
|
||||
|
||||
router.push('/rooms/none');
|
||||
};
|
||||
|
||||
const handleNotification = (roomUuid: string) => {
|
||||
if (roomListRef.value) {
|
||||
roomListRef.value.incrementUnread(roomUuid);
|
||||
|
||||
@@ -64,10 +64,6 @@ const handleAvatarError = (event: Event) => {
|
||||
img.src = defaultAvatar;
|
||||
};
|
||||
|
||||
async function handleAvatarUpdated() {
|
||||
await fetchUserData()
|
||||
}
|
||||
|
||||
const showAvatarModal = ref(false)
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -5,6 +5,11 @@ export interface User {
|
||||
avatar_url: string
|
||||
}
|
||||
|
||||
export interface UserProfile {
|
||||
uuid: string
|
||||
username: string
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
uuid: string
|
||||
username: string
|
||||
|
||||
@@ -7,7 +7,7 @@ const host = process.env.TAURI_DEV_HOST;
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig(async () => ({
|
||||
base: './',
|
||||
base: '/',
|
||||
|
||||
plugins: [vue()],
|
||||
|
||||
|
||||
Reference in New Issue
Block a user