added decline action for friend requests and room invites

This commit is contained in:
2026-01-05 20:57:25 +01:00
parent 23b1555dc7
commit 2612f1b4d1
9 changed files with 174 additions and 97 deletions

View File

@@ -25,10 +25,22 @@ export async function apiFetch<T>(
throw new Error("Session expired") throw new Error("Session expired")
} }
// Handle error responses
if (!res.ok) { if (!res.ok) {
const text = await res.text() const text = await res.text()
throw new Error(text || res.statusText) throw new Error(text || res.statusText)
} }
return res.json() as Promise<T> // Get the response as text first
const responseText = await res.text()
if (!responseText) {
return {} as T
}
try {
return JSON.parse(responseText) as T
} catch (e) {
return responseText as unknown as T
}
} }

View File

@@ -22,3 +22,10 @@ export function acceptFriendRequest(senderUuid: string) {
body: JSON.stringify({ sender_uuid: senderUuid }), body: JSON.stringify({ sender_uuid: senderUuid }),
}) })
} }
export function declineFriendRequest(senderUuid: string) {
return apiFetch<void>('/friends/decline', {
method: 'POST',
body: JSON.stringify({ sender_uuid: senderUuid }),
})
}

View File

@@ -33,3 +33,10 @@ export function acceptRoomInvite(senderUuid: string, roomUuid: string) {
body: JSON.stringify({ sender_uuid: senderUuid, room_uuid: roomUuid }), body: JSON.stringify({ sender_uuid: senderUuid, room_uuid: roomUuid }),
}) })
} }
export function declineRoomInvite(senderUuid: string, roomUuid: string) {
return apiFetch<void>('/rooms/decline', {
method: 'POST',
body: JSON.stringify({ sender_uuid: senderUuid, room_uuid: roomUuid }),
})
}

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

Before

Width:  |  Height:  |  Size: 496 B

View File

@@ -9,6 +9,8 @@
</div> </div>
<div v-else class="chat-container"> <div v-else class="chat-container">
<!-- <h2 class="room-name">{{ currentRoom?.name }}</h2> -->
<div class="messages-container" ref="messageListRef" @scroll="handleScroll"> <div class="messages-container" ref="messageListRef" @scroll="handleScroll">
<MessageList :messages="messages" /> <MessageList :messages="messages" />
</div> </div>
@@ -218,6 +220,11 @@ onUnmounted(async () => {
flex: 1; flex: 1;
} }
/* .room-name { */
/* margin: 15px 0; */
/* text-align: center; */
/* } */
.loading-more { .loading-more {
text-align: center; text-align: center;
padding: 10px; padding: 10px;

View File

@@ -53,10 +53,13 @@ notifications-room-invites = Room Invites
notifications-no-requests = No pending requests notifications-no-requests = No pending requests
notifications-no-invites = No pending invites notifications-no-invites = No pending invites
notifications-accept = Accept notifications-accept = Accept
notifications-decline = Decline
notifications-join = Join notifications-join = Join
notifications-invite-from = from: {$user} notifications-invite-from = from: {$user}
notifications-error-friend = An error occurred while accepting the request. notifications-error-friend-accept = An error occurred while accepting the request.
notifications-error-room = An error occurred while accepting the invite. notifications-error-friend-decline = An error occurred while declining the request.
notifications-error-room-accept = An error occurred while accepting the invite.
notifications-error-room-decline = An error occurred while declining the invite.
## Settings page ## Settings page
settings-title = Settings settings-title = Settings

View File

@@ -53,10 +53,13 @@ notifications-room-invites = Invitations
notifications-no-requests = Aucune demande en attente notifications-no-requests = Aucune demande en attente
notifications-no-invites = Aucune invitation en attente notifications-no-invites = Aucune invitation en attente
notifications-accept = Accepter notifications-accept = Accepter
notifications-decline = Refuser
notifications-join = Rejoindre notifications-join = Rejoindre
notifications-invite-from = de : {$user} notifications-invite-from = de : {$user}
notifications-error-friend = Erreur lors de l'acceptation de la demande. notifications-error-friend-accept = Erreur lors de l'acceptation de la demande.
notifications-error-room = Erreur lors de l'acceptation de l'invitation. notifications-error-friend-decline = Erreur lors du refus de la demande.
notifications-error-room-accept = Erreur lors de l'acceptation de l'invitation.
notifications-error-room-decline = Erreur lors du refus de l'invitation.
## Settings page ## Settings page
settings-title = Paramètres settings-title = Paramètres

View File

@@ -9,7 +9,11 @@
<ul v-if="requests.length"> <ul v-if="requests.length">
<li v-for="req in requests" :key="req.sender_uuid"> <li v-for="req in requests" :key="req.sender_uuid">
<span>{{ req.sender_username }}</span> <span>{{ req.sender_username }}</span>
<button @click="acceptFriend(req.sender_uuid)">{{ $t('notifications-accept') }}</button> <div class="actions">
<button @click="acceptFriend(req.sender_uuid)">{{ $t('notifications-accept') }}</button>
<button class="decline-btn" @click="declineFriend(req.sender_uuid)">{{ $t('notifications-decline')
}}</button>
</div>
</li> </li>
</ul> </ul>
<p v-else>{{ $t('notifications-no-requests') }}</p> <p v-else>{{ $t('notifications-no-requests') }}</p>
@@ -22,7 +26,11 @@
<li v-for="inv in invites" :key="inv.sender_uuid"> <li v-for="inv in invites" :key="inv.sender_uuid">
<span>{{ inv.room_name }}</span> <span>{{ inv.room_name }}</span>
<span>{{ $t('notifications-invite-from', { user: inv.sender_username }) }}</span> <span>{{ $t('notifications-invite-from', { user: inv.sender_username }) }}</span>
<button @click="acceptRoom(inv.sender_uuid, inv.room_uuid)">{{ $t('notifications-join') }}</button> <div class="actions">
<button @click="acceptRoom(inv.sender_uuid, inv.room_uuid)">{{ $t('notifications-join') }}</button>
<button class="decline-btn" @click="declineRoom(inv.sender_uuid, inv.room_uuid)">{{
$t('notifications-decline') }}</button>
</div>
</li> </li>
</ul> </ul>
<p v-else>{{ $t('notifications-no-invites') }}</p> <p v-else>{{ $t('notifications-no-invites') }}</p>
@@ -32,8 +40,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
import { fetchFriendRequests, acceptFriendRequest } from '../api/friends' import { fetchFriendRequests, acceptFriendRequest, declineFriendRequest } from '../api/friends'
import { fetchRoomInvites, acceptRoomInvite } from '../api/rooms.ts' import { fetchRoomInvites, acceptRoomInvite, declineRoomInvite } from '../api/rooms.ts'
import { useNotifications } from '../store' import { useNotifications } from '../store'
import { useFluent } from 'fluent-vue'; import { useFluent } from 'fluent-vue';
@@ -54,7 +62,17 @@ async function acceptFriend(senderUuid: string) {
requests.value = requests.value.filter(r => r.sender_uuid !== senderUuid) requests.value = requests.value.filter(r => r.sender_uuid !== senderUuid)
// fetchFriends().then(f => (friends.value = f)) // fetchFriends().then(f => (friends.value = f))
} catch (err) { } catch (err) {
errorMessage.value = $t('notifications-error-friend') // TODO: handle this case errorMessage.value = $t('notifications-error-friend-accept')
}
}
async function declineFriend(senderUuid: string) {
try {
await declineFriendRequest(senderUuid)
requests.value = requests.value.filter(r => r.sender_uuid !== senderUuid)
// fetchFriends().then(f => (friends.value = f))
} catch (err) {
errorMessage.value = $t('notifications-error-friend-decline')
} }
} }
@@ -62,9 +80,18 @@ async function acceptRoom(senderUuid: string, roomUuid: string) {
try { try {
await acceptRoomInvite(senderUuid, roomUuid) await acceptRoomInvite(senderUuid, roomUuid)
invites.value = invites.value.filter(r => r.room_uuid !== roomUuid) invites.value = invites.value.filter(r => r.room_uuid !== roomUuid)
// fetchFriends().then(f => (friends.value = f))
} catch (err) { } catch (err) {
errorMessage.value = $t('notifications-error-room') // TODO: handle this case errorMessage.value = $t('notifications-error-room-accept')
throw err
}
}
async function declineRoom(senderUuid: string, roomUuid: string) {
try {
await declineRoomInvite(senderUuid, roomUuid)
invites.value = invites.value.filter(r => r.room_uuid !== roomUuid)
} catch (err) {
errorMessage.value = $t('notifications-error-room-decline')
throw err throw err
} }
} }
@@ -144,6 +171,17 @@ async function acceptRoom(senderUuid: string, roomUuid: string) {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.decline-btn {
color: var(--text);
background-color: transparent;
border: 1px solid var(--accent);
border-radius: var(--radius);
}
.decline-btn:hover {
background-color: rgba(255, 255, 255, 0.05);
}
@media (max-width: 720px) { @media (max-width: 720px) {
.friend-requests-container { .friend-requests-container {
flex-direction: column; flex-direction: column;

View File

@@ -1,34 +1,35 @@
<template> <template>
<div class="settings-page"> <div class="settings-page">
<h1>{{ $t('settings-title') }}</h1> <h1>{{ $t('settings-title') }}</h1>
<UpdateAccountModal v-if="showUpdateModal" :user="user" @close="showUpdateModal = false" @updated="fetchUserData" /> <UpdateAccountModal v-if="showUpdateModal" :user="user" @close="showUpdateModal = false"
@updated="fetchUserData" />
<h2>{{ $t('settings-account') }}</h2> <h2>{{ $t('settings-account') }}</h2>
<div v-if="user" class="info-card"> <div v-if="user" class="info-card">
<button class="update-btn" @click="showUpdateModal = true">{{ $t('settings-update-btn') }}</button> <button class="update-btn" @click="showUpdateModal = true">{{ $t('settings-update-btn') }}</button>
<p><strong>{{ $t('settings-label-username') }}</strong> {{ user.username }}</p> <p><strong>{{ $t('settings-label-username') }}</strong> {{ user.username }}</p>
<p><strong>{{ $t('settings-label-email') }}</strong> {{ user.email }}</p> <p><strong>{{ $t('settings-label-email') }}</strong> {{ user.email }}</p>
</div> </div>
<div v-else class="loading-state"> <div v-else class="loading-state">
<p>{{ $t('settings-loading') }}</p> <p>{{ $t('settings-loading') }}</p>
</div> </div>
<h2>{{ $t('settings-language') }}</h2> <h2>{{ $t('settings-language') }}</h2>
<div class="input-group"> <div class="input-group">
<div class="lang-grid"> <div class="lang-grid">
<button v-for="lang in languages" :key="lang.code" class="lang-btn" <button v-for="lang in languages" :key="lang.code" class="lang-btn"
:class="{ active: currentLang === lang.code }" @click="changeLanguage(lang.code)"> :class="{ active: currentLang === lang.code }" @click="changeLanguage(lang.code)">
{{ lang.name }} {{ lang.name }}
</button>
</div>
</div>
<button class="logout-btn" @click="logout">
<i class="fa-solid fa-right-from-bracket"></i>
<span>{{ $t('settings-logout-btn') }}</span>
</button> </button>
</div>
</div> </div>
<button class="logout-btn" @click="logout">
<i class="fa-solid fa-right-from-bracket"></i>
<span>{{ $t('settings-logout-btn') }}</span>
</button>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -51,112 +52,112 @@ const currentLang = ref('')
const languages = computed(() => getSupportedLanguagesMetadata()) const languages = computed(() => getSupportedLanguagesMetadata())
async function fetchUserData() { async function fetchUserData() {
try { try {
const auth = await getAuthData() const auth = await getAuthData()
user.value = auth.user user.value = auth.user
} catch (err) { } catch (err) {
console.error("Failed to load user data:", err) console.error("Failed to load user data:", err)
} }
} }
onMounted(async () => { onMounted(async () => {
const pref = await getLocalePreference() const pref = await getLocalePreference()
// Synchronize the UI state with the actual active language // Synchronize the UI state with the actual active language
currentLang.value = pref || (navigator.language.split('-')[0]) currentLang.value = pref || (navigator.language.split('-')[0])
fetchUserData() fetchUserData()
}) })
async function changeLanguage(code: string) { async function changeLanguage(code: string) {
const actual = setLanguage(code) const actual = setLanguage(code)
currentLang.value = actual currentLang.value = actual
await saveLocalePreference(actual) await saveLocalePreference(actual)
} }
function logout() { function logout() {
authLogout() authLogout()
router.push('/login') router.push('/login')
} }
</script> </script>
<style scoped> <style scoped>
.settings-page { .settings-page {
max-width: 720px; max-width: 720px;
margin: 0 auto; margin: 0 auto;
padding: 2rem 1.5rem; padding: 2rem 1.5rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
} }
h2 { h2 {
margin-top: 20px; margin-top: 20px;
} }
.info-card { .info-card {
position: relative; position: relative;
background: var(--panel); background: var(--panel);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: var(--radius); border-radius: var(--radius);
padding: 1rem; padding: 1rem;
} }
.update-btn { .update-btn {
position: absolute; position: absolute;
right: 10px; right: 10px;
top: 10px; top: 10px;
} }
.lang-grid { .lang-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 0.5rem; gap: 0.5rem;
margin-top: 0.5rem; margin-top: 0.5rem;
} }
.lang-btn { .lang-btn {
background: var(--panel); background: var(--panel);
border: 1px solid var(--border); border: 1px solid var(--border);
color: var(--text); color: var(--text);
margin: 0; margin: 0;
} }
.lang-btn.active { .lang-btn.active {
background: var(--accent); background: var(--accent);
color: #000; color: #000;
border-color: var(--accent); border-color: var(--accent);
} }
.logout-btn { .logout-btn {
cursor: pointer; cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
margin-top: 30px; margin-top: 30px;
padding: 10px 20px; padding: 10px 20px;
color: var(--text); color: var(--text);
background-color: transparent; background-color: transparent;
border: 1px solid #ff5050; border: 1px solid var(--error);
border-radius: 8px; border-radius: var(--radius);
width: fit-content; width: fit-content;
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.logout-btn:hover { .logout-btn:hover {
color: rgba(255, 80, 80, 0.8); color: rgba(255, 80, 80, 0.8);
} }
.logout-btn:hover i { .logout-btn:hover i {
color: rgba(255, 80, 80, 0.8); color: rgba(255, 80, 80, 0.8);
} }
.logout-btn i { .logout-btn i {
font-size: 1.25rem; font-size: 1.25rem;
} }
.loading-state { .loading-state {
padding: 2rem; padding: 2rem;
text-align: center; text-align: center;
color: var(--muted); color: var(--muted);
} }
</style> </style>