added roominvites in frontend in a notifications page and added clap to define backend port

This commit is contained in:
2025-12-31 12:14:46 +01:00
parent fb514f1efe
commit e7a575ae03
12 changed files with 258 additions and 145 deletions

View File

@@ -1,5 +1,5 @@
import { fetch } from '@tauri-apps/plugin-http';
import { initAuth, logout } from '../store.ts'
import { getAuthData, clearAuthData } from '../authStore'
import { API } from '../main.ts'
import router from '../router'
@@ -7,12 +7,11 @@ export async function apiFetch<T>(
path: string,
options: RequestInit = {}
): Promise<T> {
const auth = await initAuth()
const auth = await getAuthData()
const res = await fetch(`${API}${path}`, {
method: options.method || 'GET',
body: options.body,
...options,
method: options.method || 'GET',
headers: {
'Content-Type': 'application/json',
...(auth.token ? { Authorization: `Bearer ${auth.token}` } : {}),
@@ -21,7 +20,7 @@ export async function apiFetch<T>(
})
if (res.status === 401) {
await logout()
await clearAuthData()
router.push('/login')
throw new Error("Session expired")
}

View File

@@ -1,8 +1,8 @@
import { apiFetch } from './client'
import type { Room } from '../types'
import type { Room, RoomInvite } from '../types'
export function fetchRooms(userUuid: string) {
return apiFetch<Room[]>(`/rooms/${userUuid}`)
export function fetchRooms() {
return apiFetch<Room[]>(`/rooms`)
}
export function createRoom(name: string, global: boolean) {
@@ -12,3 +12,20 @@ export function createRoom(name: string, global: boolean) {
})
}
export function fetchRoomInvites() {
return apiFetch<RoomInvite[]>('/rooms/invites')
}
export function sendRoomInvite(receiverUsername: string, roomUuid: string) {
return apiFetch<void>('/rooms/invite', {
method: 'POST',
body: JSON.stringify({ receiver_username: receiverUsername, room_uuid: roomUuid }),
});
}
export function acceptRoomInvite(senderUuid: string, roomUuid: string) {
return apiFetch<void>('/rooms/join', {
method: 'POST',
body: JSON.stringify({ sender_uuid: senderUuid, room_uuid: roomUuid }),
})
}

41
src/authStore.ts Normal file
View File

@@ -0,0 +1,41 @@
import { load, Store } from '@tauri-apps/plugin-store'
let store: Store | null = null
async function getStore() {
if (!store) store = await load('store.json')
return store
}
export async function getAuthData() {
const s = await getStore()
const token = await s.get<string>('token')
const uuid = await s.get<string>('uuid')
return { token: token || null, uuid: uuid || null, isAuthenticated: !!token }
}
export async function saveAuthData(token: string, uuid: string) {
const s = await getStore()
await s.set('token', token)
await s.set('uuid', uuid)
await s.save()
}
export async function clearAuthData() {
const s = await getStore()
await s.delete('token')
await s.delete('uuid')
await s.save()
}
export async function setLastRoom(uuid: string) {
if (!uuid || uuid === 'none') return
const s = await getStore()
await s.set('last_room_uuid', uuid)
await s.save()
}
export async function getLastRoom(): Promise<string | null> {
const s = await getStore()
return (await s.get<string>('last_room_uuid')) ?? null
}

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, nextTick, defineExpose } from 'vue'
import { ref, nextTick } from 'vue'
const content = ref('')
const textareaRef = ref<HTMLTextAreaElement | null>(null)

View File

@@ -1,7 +1,7 @@
<template>
<div>
<nav id="bottom-nav">
<router-link to="/rooms/none" class="nav-item" :class="{ 'router-link-active': $route.name === 'chat' }">
<router-link to="/" class="nav-item" :class="{ 'router-link-active': $route.name === 'chat' }">
<i class="fa-solid fa-message"></i>
</router-link>
@@ -9,6 +9,10 @@
<i class="fa-solid fa-user-group"></i>
</router-link>
<router-link to="/notifications" class="nav-item">
<i class="fa-solid fa-bell"></i>
</router-link>
<button class="nav-item logout" @click="logout">
<i class="fa-solid fa-right-from-bracket"></i>
</button>

View File

@@ -15,5 +15,5 @@ async function init() {
init()
export const API = 'http://192.168.1.147:8081'
export const API_WS = 'ws://192.168.1.147:8081/ws'
export const API = 'http://127.0.0.1:8080'
export const API_WS = 'ws://127.0.0.1:8080/ws'

View File

@@ -14,57 +14,31 @@
</div>
</header>
<div class="friends-requests-container">
<!-- Friends List -->
<div class="friends-list">
<h2>Your Friends</h2>
<ul>
<li v-for="friend in friends" :key="friend.uuid">
{{ friend.username }}
</li>
</ul>
</div>
<!-- Friend Request List -->
<div class="requests">
<h2>Friend Requests</h2>
<ul v-if="requests.length">
<li v-for="req in requests" :key="req.sender_uuid">
<span>{{ req.sender_username }}</span>
<button @click="accept(req.sender_uuid)">Accept</button>
</li>
</ul>
<p v-else>No pending requests</p>
</div>
<!-- Friends List -->
<div class="friends-list">
<h2>Your Friends</h2>
<ul>
<li v-for="friend in friends" :key="friend.uuid">
{{ friend.username }}
</li>
</ul>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { fetchFriends, fetchFriendRequests, acceptFriendRequest, sendFriendRequest } from '../api/friends'
import type { Friend, FriendRequest } from '../types'
import { fetchFriends, sendFriendRequest } from '../api/friends'
import type { Friend } from '../types'
const friends = ref<Friend[]>([])
const requests = ref<FriendRequest[]>([])
const username = ref('')
const errorMessage = ref('')
onMounted(async () => {
friends.value = await fetchFriends()
requests.value = await fetchFriendRequests()
})
async function accept(senderUuid: string) {
try {
await acceptFriendRequest(senderUuid)
requests.value = requests.value.filter(r => r.sender_uuid !== senderUuid)
fetchFriends().then(f => (friends.value = f))
} catch (err) {
errorMessage.value = 'An error occurred while accepting the request.' // TODO: handle this supposedly impossible case
}
}
async function send() {
if (!username.value) {
// errorMessage.value = 'Username is required.'
@@ -125,14 +99,7 @@ async function send() {
font-size: 0.9rem;
}
.friends-requests-container {
display: flex;
gap: 2rem;
flex-wrap: wrap;
}
.friends-list,
.requests {
.friends-list {
min-width: 250px;
background: var(--panel);
border: 1px solid var(--border);
@@ -140,14 +107,12 @@ async function send() {
padding: 1rem;
}
.friends-list ul,
.requests ul {
.friends-list ul {
list-style: none;
padding: 0;
}
.friends-list li,
.requests li {
.friends-list li {
background: var(--panel-light);
border: 1px solid var(--border);
border-radius: var(--radius);
@@ -155,41 +120,8 @@ async function send() {
margin-bottom: 0.5rem;
}
.requests {
max-height: 500px;
overflow-y: auto;
}
.requests li {
display: flex;
justify-content: space-between;
align-items: center;
background: var(--panel-light);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 0.75rem 1rem;
margin-bottom: 0.5rem;
}
.requests li button {
padding-top: 5px;
padding-bottom: 5px;
}
.requests p {
font-size: 0.9rem;
color: gray;
}
.friends-list h2,
.requests h2 {
.friends-list h2 {
font-size: 1.25rem;
margin-bottom: 1rem;
}
@media (max-width: 720px) {
.friends-requests-container {
flex-direction: column;
}
}
</style>

View File

@@ -0,0 +1,149 @@
<template>
<div class="notifications-page">
<h1>Notifications</h1>
<p v-if="errorMessage" class="error-message">{{ errorMessage }}</p>
<!-- Friend Request List -->
<div class="list">
<h2>Friend Requests</h2>
<ul v-if="requests.length">
<li v-for="req in requests" :key="req.sender_uuid">
<span>{{ req.sender_username }}</span>
<button @click="acceptFriend(req.sender_uuid)">Accept</button>
</li>
</ul>
<p v-else>No pending requests</p>
</div>
<!-- Room Invites List -->
<div class="list">
<h2>Room Invites</h2>
<ul v-if="invites.length">
<li v-for="inv in invites" :key="inv.sender_uuid">
<span>{{ inv.room_name }}</span>
<span>from: {{ inv.sender_username }}</span>
<button @click="acceptRoom(inv.sender_uuid, inv.room_uuid)">Join</button>
</li>
</ul>
<p v-else>No pending invites</p>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { fetchFriendRequests, acceptFriendRequest } from '../api/friends'
import { fetchRoomInvites, acceptRoomInvite } from '../api/rooms.ts'
import type { FriendRequest, RoomInvite } from '../types'
const requests = ref<FriendRequest[]>([])
const invites = ref<RoomInvite[]>([])
const errorMessage = ref('')
onMounted(async () => {
requests.value = await fetchFriendRequests()
invites.value = await fetchRoomInvites()
})
async function acceptFriend(senderUuid: string) {
try {
await acceptFriendRequest(senderUuid)
requests.value = requests.value.filter(r => r.sender_uuid !== senderUuid)
// fetchFriends().then(f => (friends.value = f))
} catch (err) {
errorMessage.value = 'An error occurred while accepting the request.' // TODO: handle this case
}
}
async function acceptRoom(senderUuid: string, roomUuid: string) {
try {
await acceptRoomInvite(senderUuid, roomUuid)
invites.value = invites.value.filter(r => r.room_uuid !== roomUuid)
// fetchFriends().then(f => (friends.value = f))
} catch (err) {
errorMessage.value = 'An error occurred while accepting the invite.' // TODO: handle this case
}
}
</script>
<style scoped>
.notifications-page {
max-width: 720px;
margin: 0 auto;
padding: 2rem 1.5rem;
display: flex;
flex-direction: column;
gap: 2rem;
}
.error-message {
color: red;
font-size: 0.9rem;
}
.friend-requests-container {
display: flex;
justify-content: center;
gap: 2rem;
flex-wrap: wrap;
}
.list {
min-width: 250px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1rem;
}
.list ul {
list-style: none;
padding: 0;
}
.list li {
background: var(--panel-light);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 0.75rem 1rem;
margin-bottom: 0.5rem;
}
.list {
max-height: 500px;
overflow-y: auto;
}
.list li {
display: flex;
justify-content: space-between;
align-items: center;
background: var(--panel-light);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 0.75rem 1rem;
margin-bottom: 0.5rem;
}
.list li button {
padding-top: 5px;
padding-bottom: 5px;
}
.list p {
font-size: 0.9rem;
color: gray;
}
.list h2 {
font-size: 1.25rem;
margin-bottom: 1rem;
}
@media (max-width: 720px) {
.friend-requests-container {
flex-direction: column;
}
}
</style>

View File

@@ -4,6 +4,7 @@ import { initAuth, getLastRoom, setLastRoom } from './store'
import LoginPage from './pages/LoginPage.vue'
import ChatPage from './pages/ChatPage.vue'
import FriendListPage from './pages/FriendListPage.vue'
import NofificationsPage from './pages/NotificationsPage.vue'
const router = createRouter({
history: createWebHistory(),
@@ -21,7 +22,8 @@ const router = createRouter({
component: ChatPage,
props: true
},
{ path: '/friendlist', component: FriendListPage }
{ path: '/friendlist', component: FriendListPage },
{ path: '/notifications', component: NofificationsPage }
],
})

View File

@@ -1,36 +1,10 @@
import { load, Store } from '@tauri-apps/plugin-store'
import type { LoginResponse } from './types'
import { apiFetch } from './api/client'
import type { LoginResponse } from './types'
import * as authStore from './authStore'
let store: Store | null = null
async function getStore() {
if (!store) {
store = await load('store.json')
}
return store
}
export async function setLastRoom(uuid: string) {
if (!uuid || uuid === 'none') return
const s = await getStore()
await s.set('last_room_uuid', uuid)
await s.save()
}
export async function getLastRoom(): Promise<string | null> {
const s = await getStore()
const lastRoom = await s.get<string>('last_room_uuid')
return lastRoom ?? null
}
export async function initAuth() {
const s = await getStore()
const token = await s.get<string>('token')
const uuid = await s.get<string>('uuid')
return { token: token || null, uuid: uuid || null, isAuthenticated: !!token }
}
export const initAuth = authStore.getAuthData
export const getLastRoom = authStore.getLastRoom
export const setLastRoom = authStore.setLastRoom
export async function login(email: string, username: string, password: string) {
const res: LoginResponse = await apiFetch('/login', {
@@ -38,35 +12,23 @@ export async function login(email: string, username: string, password: string) {
body: JSON.stringify({ email, username, password }),
})
const s = await getStore()
await s.set('token', res.token)
await s.set('uuid', res.uuid)
await s.save()
await authStore.saveAuthData(res.token, res.uuid)
return { token: res.token, uuid: res.uuid, isAuthenticated: true }
}
export async function logout() {
const s = await getStore()
await s.delete('token')
await s.delete('uuid')
await s.save()
await authStore.clearAuthData()
return { token: null, uuid: null, isAuthenticated: false }
}
export async function validateToken(): Promise<boolean> {
const auth = await initAuth()
const auth = await authStore.getAuthData()
if (!auth.token) return false
try {
await apiFetch('/validate-token')
return true
} catch (e: any) {
// Only logout if token is bad
if (e.message.includes('401')) {
await logout()
return false
}
return true
} catch (e) {
return false
}
}

View File

@@ -27,3 +27,10 @@ export interface FriendRequest {
sender_uuid: string
sender_username: string
}
export interface RoomInvite {
room_uuid: string
room_name: string
sender_uuid: string
sender_username: string
}