added room invite button in rooms that should have it
This commit is contained in:
@@ -19,7 +19,7 @@ export async function apiFetch<T>(
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (res.status === 401) {
|
if (res.status === 401 && auth.token) {
|
||||||
await clearAuthData()
|
await clearAuthData()
|
||||||
router.push('/login')
|
router.push('/login')
|
||||||
throw new Error("Session expired")
|
throw new Error("Session expired")
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ export function fetchRooms() {
|
|||||||
return apiFetch<Room[]>(`/rooms`)
|
return apiFetch<Room[]>(`/rooms`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fetchRoomInfo(uuid: string) {
|
||||||
|
return apiFetch<Room>(`/rooms/${uuid}`)
|
||||||
|
}
|
||||||
|
|
||||||
export function createRoom(name: string, global: boolean) {
|
export function createRoom(name: string, global: boolean) {
|
||||||
return apiFetch<Room>('/rooms', {
|
return apiFetch<Room>('/rooms', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { load, Store } from '@tauri-apps/plugin-store'
|
import { load, Store } from '@tauri-apps/plugin-store'
|
||||||
|
import { User } from './types'
|
||||||
|
|
||||||
let store: Store | null = null
|
let store: Store | null = null
|
||||||
|
|
||||||
@@ -10,21 +11,22 @@ async function getStore() {
|
|||||||
export async function getAuthData() {
|
export async function getAuthData() {
|
||||||
const s = await getStore()
|
const s = await getStore()
|
||||||
const token = await s.get<string>('token')
|
const token = await s.get<string>('token')
|
||||||
const uuid = await s.get<string>('uuid')
|
const user = await s.get<User>('user')
|
||||||
return { token: token || null, uuid: uuid || null, isAuthenticated: !!token }
|
return { token: token || null, user: user || null, isAuthenticated: !!token }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveAuthData(token: string, uuid: string) {
|
export async function saveAuthData(token: string, user: User) {
|
||||||
const s = await getStore()
|
const s = await getStore()
|
||||||
await s.set('token', token)
|
await s.set('token', token)
|
||||||
await s.set('uuid', uuid)
|
await s.set('user', user)
|
||||||
await s.save()
|
await s.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function clearAuthData() {
|
export async function clearAuthData() {
|
||||||
const s = await getStore()
|
const s = await getStore()
|
||||||
await s.delete('token')
|
await s.delete('token')
|
||||||
await s.delete('uuid')
|
await s.delete('user')
|
||||||
|
await s.delete('last_room_uuid')
|
||||||
await s.save()
|
await s.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<InvitePeopleModal v-if="showInviteModal" :room_uuid=props.uuid @close="showInviteModal = false" />
|
||||||
|
|
||||||
<div v-if="uuid === 'none'" class="no-room">
|
<div v-if="uuid === 'none'" class="no-room">
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<i class="fa-solid fa-comments"></i>
|
<i class="fa-solid fa-comments"></i>
|
||||||
@@ -12,28 +14,45 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
|
<button v-if="isOwner && !currentRoom?.global" class="invite-btn" @click="showInviteModal = true"
|
||||||
|
title="Invite people">
|
||||||
|
<i class="fa-solid fa-users"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
<MessageInput ref="messageInputRef" @send="onSend" />
|
<MessageInput ref="messageInputRef" @send="onSend" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted, watch, nextTick } from "vue";
|
import { ref, onMounted, onUnmounted, watch, nextTick, computed } from "vue";
|
||||||
import { fetchMessages, sendMessage, getWsToken } from "../api/messages";
|
import { fetchMessages, sendMessage, getWsToken } from "../api/messages";
|
||||||
import type { Message } from "../types";
|
import type { Message, Room, User } from "../types";
|
||||||
import { API_WS } from '../main.ts';
|
import { API_WS } from '../main.ts';
|
||||||
import MessageList from "./MessageList.vue";
|
import MessageList from "./MessageList.vue";
|
||||||
import MessageInput from "./MessageInput.vue";
|
import MessageInput from "./MessageInput.vue";
|
||||||
|
import InvitePeopleModal from './InvitePeopleModal.vue';
|
||||||
|
|
||||||
import WebSocket from '@tauri-apps/plugin-websocket';
|
import WebSocket from '@tauri-apps/plugin-websocket';
|
||||||
|
import { getAuthData } from "../authStore.ts";
|
||||||
|
import { fetchRoomInfo } from "../api/rooms.ts";
|
||||||
|
|
||||||
const props = defineProps<{ uuid: string }>();
|
const props = defineProps<{ uuid: string }>();
|
||||||
const messages = ref<Message[]>([]);
|
const messages = ref<Message[]>([]);
|
||||||
const messageListRef = ref<HTMLElement | null>(null);
|
const messageListRef = ref<HTMLElement | null>(null);
|
||||||
const messageInputRef = ref<InstanceType<typeof MessageInput> | null>(null);
|
const messageInputRef = ref<InstanceType<typeof MessageInput> | null>(null);
|
||||||
|
const currentUser = ref<User | null>(null);
|
||||||
|
const currentRoom = ref<Room | null>(null);
|
||||||
|
|
||||||
|
const showInviteModal = ref(false);
|
||||||
|
|
||||||
let socket: WebSocket | null = null;
|
let socket: WebSocket | null = null;
|
||||||
|
|
||||||
|
const isOwner = computed(() => {
|
||||||
|
if (!currentUser.value || !currentRoom.value) return false;
|
||||||
|
return currentUser.value.uuid === currentRoom.value.owner_uuid;
|
||||||
|
});
|
||||||
|
|
||||||
const handleGlobalKeyDown = (event: KeyboardEvent) => {
|
const handleGlobalKeyDown = (event: KeyboardEvent) => {
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
const active = document.activeElement?.tagName.toLowerCase();
|
const active = document.activeElement?.tagName.toLowerCase();
|
||||||
@@ -51,24 +70,33 @@ async function initializeRoom() {
|
|||||||
socket = null;
|
socket = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
messages.value = [];
|
// messages.value = [];
|
||||||
|
currentRoom.value = null;
|
||||||
|
|
||||||
if (props.uuid === 'none') return;
|
if (props.uuid === 'none') return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
messages.value = await fetchMessages(props.uuid);
|
// 5. Fetch Room Details and Messages in parallel
|
||||||
|
const [msgs, roomInfo, auth] = await Promise.all([
|
||||||
|
fetchMessages(props.uuid),
|
||||||
|
fetchRoomInfo(props.uuid),
|
||||||
|
getAuthData()
|
||||||
|
]);
|
||||||
|
|
||||||
|
messages.value = msgs;
|
||||||
|
currentRoom.value = roomInfo;
|
||||||
|
currentUser.value = auth.user;
|
||||||
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
|
|
||||||
const wsToken = await getWsToken(props.uuid);
|
const wsToken = await getWsToken(props.uuid);
|
||||||
|
|
||||||
const url = `${API_WS}/rooms/${props.uuid}?token=${wsToken}`;
|
const url = `${API_WS}/rooms/${props.uuid}?token=${wsToken}`;
|
||||||
socket = await WebSocket.connect(url);
|
socket = await WebSocket.connect(url);
|
||||||
|
|
||||||
socket.addListener((msg) => {
|
socket.addListener((msg) => {
|
||||||
if (msg.type === 'Text') {
|
if (msg.type === 'Text') {
|
||||||
const data: Message = JSON.parse(msg.data);
|
const data: Message = JSON.parse(msg.data);
|
||||||
|
|
||||||
if (!messages.value.some(m => m.uuid === data.uuid)) {
|
if (!messages.value.some(m => m.uuid === data.uuid)) {
|
||||||
messages.value.push(data);
|
messages.value.push(data);
|
||||||
nextTick().then(scrollToBottom);
|
nextTick().then(scrollToBottom);
|
||||||
@@ -76,9 +104,8 @@ async function initializeRoom() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("WebSocket Connected successfully via Rust layer");
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("WebSocket connection failed:", err);
|
console.error("Room initialization failed:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,10 +151,34 @@ onUnmounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.input-container {
|
.input-container {
|
||||||
padding: 1rem;
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: flex-end;
|
||||||
|
padding: 10px;
|
||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Ensure the MessageInput component expands to fill the width */
|
||||||
|
:deep(.input-container > *:last-child) {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invite-btn {
|
||||||
|
margin: 0;
|
||||||
|
padding: 18px;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.no-room {
|
.no-room {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
123
src/components/InvitePeopleModal.vue
Normal file
123
src/components/InvitePeopleModal.vue
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
<template>
|
||||||
|
<div class="backdrop" @click.self="emit('close')">
|
||||||
|
<div class="modal">
|
||||||
|
<h2>Invite People</h2>
|
||||||
|
|
||||||
|
<p v-if="errorMessage" class="error-message">{{ errorMessage }}</p>
|
||||||
|
|
||||||
|
<input v-model="receiverUsername" placeholder="username" />
|
||||||
|
|
||||||
|
<label class="checkbox">
|
||||||
|
<input type="checkbox" v-model="requestFriend" />
|
||||||
|
<span>Also send a friend request</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button @click="emit('close')" class="secondary">Cancel</button>
|
||||||
|
<button @click="submit">Send</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { sendRoomInvite } from '../api/rooms'
|
||||||
|
import { sendFriendRequest } from '../api/friends';
|
||||||
|
|
||||||
|
const props = defineProps<{ room_uuid: string }>();
|
||||||
|
|
||||||
|
const errorMessage = ref('')
|
||||||
|
const receiverUsername = ref('')
|
||||||
|
const requestFriend = ref(false)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'close'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
errorMessage.value = ''
|
||||||
|
const username = receiverUsername.value.trim()
|
||||||
|
|
||||||
|
if (!username) {
|
||||||
|
// errorMessage.value = 'Username is required.'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sendRoomInvite(username, props.room_uuid)
|
||||||
|
|
||||||
|
if (requestFriend.value) {
|
||||||
|
await sendFriendRequest(username)
|
||||||
|
}
|
||||||
|
|
||||||
|
receiverUsername.value = ''
|
||||||
|
requestFriend.value = false
|
||||||
|
emit('close')
|
||||||
|
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(err)
|
||||||
|
errorMessage.value = err?.message || err || 'An error occurred'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</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: 420px;
|
||||||
|
max-height: 500px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal h2 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
color: red;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,7 +2,8 @@
|
|||||||
<div class="room-list">
|
<div class="room-list">
|
||||||
<header class="rooms-header">
|
<header class="rooms-header">
|
||||||
<h2>Rooms</h2>
|
<h2>Rooms</h2>
|
||||||
<button class="create-btn" @click="showCreate = true"><i class="fa-solid fa-plus"></i></button>
|
<button class="create-btn" @click="showCreate = true" title="Create a room"><i
|
||||||
|
class="fa-solid fa-plus"></i></button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<CreateRoomModal v-if="showCreate" @close="showCreate = false" @created="rooms.push($event)" />
|
<CreateRoomModal v-if="showCreate" @close="showCreate = false" @created="rooms.push($event)" />
|
||||||
@@ -22,7 +23,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { initAuth } from '../store.ts';
|
|
||||||
import { fetchRooms } from '../api/rooms';
|
import { fetchRooms } from '../api/rooms';
|
||||||
import type { Room } from '../types';
|
import type { Room } from '../types';
|
||||||
import CreateRoomModal from './CreateRoomModal.vue';
|
import CreateRoomModal from './CreateRoomModal.vue';
|
||||||
@@ -34,8 +34,7 @@ const rooms = ref<Room[]>([]);
|
|||||||
const emit = defineEmits(['select-room']);
|
const emit = defineEmits(['select-room']);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const auth = await initAuth();
|
rooms.value = await fetchRooms();
|
||||||
rooms.value = await fetchRooms(auth.uuid!);
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
<div v-if="isSidebarOpen" class="sidebar-overlay" @click="isSidebarOpen = false"></div>
|
<div v-if="isSidebarOpen" class="sidebar-overlay" @click="isSidebarOpen = false"></div>
|
||||||
|
|
||||||
<main class="chat-window-container">
|
<main class="chat-window-container" :class="{ 'sidebar-is-open': isSidebarOpen }">
|
||||||
<ChatWindow :uuid="uuid" />
|
<ChatWindow :uuid="uuid" />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
@@ -99,6 +99,12 @@ const handleRoomSelection = () => {
|
|||||||
font-size: 1.6rem;
|
font-size: 1.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 721px) {
|
||||||
|
.chat-window-container.sidebar-is-open {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
.sidebar {
|
.sidebar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ async function acceptRoom(senderUuid: string, roomUuid: string) {
|
|||||||
// fetchFriends().then(f => (friends.value = f))
|
// fetchFriends().then(f => (friends.value = f))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errorMessage.value = 'An error occurred while accepting the invite.' // TODO: handle this case
|
errorMessage.value = 'An error occurred while accepting the invite.' // TODO: handle this case
|
||||||
|
throw err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { apiFetch } from './api/client'
|
import { apiFetch } from './api/client'
|
||||||
import type { LoginResponse } from './types'
|
import type { LoginResponse, User } from './types'
|
||||||
import * as authStore from './authStore'
|
import * as authStore from './authStore'
|
||||||
|
|
||||||
export const initAuth = authStore.getAuthData
|
export const initAuth = authStore.getAuthData
|
||||||
@@ -12,7 +12,11 @@ export async function login(email: string, username: string, password: string) {
|
|||||||
body: JSON.stringify({ email, username, password }),
|
body: JSON.stringify({ email, username, password }),
|
||||||
})
|
})
|
||||||
|
|
||||||
await authStore.saveAuthData(res.token, res.uuid)
|
let user: User = {
|
||||||
|
uuid: res.uuid,
|
||||||
|
username: username
|
||||||
|
};
|
||||||
|
await authStore.saveAuthData(res.token, user)
|
||||||
return { token: res.token, uuid: res.uuid, isAuthenticated: true }
|
return { token: res.token, uuid: res.uuid, isAuthenticated: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
10
src/types.ts
10
src/types.ts
@@ -1,3 +1,8 @@
|
|||||||
|
export interface User {
|
||||||
|
uuid: string
|
||||||
|
username: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface LoginResponse {
|
export interface LoginResponse {
|
||||||
uuid: string
|
uuid: string
|
||||||
token: string
|
token: string
|
||||||
@@ -5,9 +10,10 @@ export interface LoginResponse {
|
|||||||
|
|
||||||
export interface Room {
|
export interface Room {
|
||||||
uuid: string
|
uuid: string
|
||||||
owner_name: number
|
owner_name: string
|
||||||
|
owner_uuid: string
|
||||||
name: string
|
name: string
|
||||||
globa: boolean
|
global: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Message {
|
export interface Message {
|
||||||
|
|||||||
Reference in New Issue
Block a user