Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion app/Events/ChatMessageDeleted.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ public function __construct(int $messageId, ?string $deletedAt = null)
*/
public function broadcastOn(): array
{
$channel = app()->environment('production') ? 'global-chat' : app()->environment().'.global-chat';

return [
new Channel('global-chat'),
new Channel($channel),
];
}

Expand Down
4 changes: 3 additions & 1 deletion app/Events/ChatMessageReacted.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ public function __construct(int $messageId, array $reactions)
*/
public function broadcastOn(): array
{
$channel = app()->environment('production') ? 'global-chat' : app()->environment().'.global-chat';

return [
new Channel('global-chat'),
new Channel($channel),
];
}

Expand Down
4 changes: 3 additions & 1 deletion app/Events/ChatMessageSent.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,10 @@ public function __construct(ChatMessage $chatMessage)
*/
public function broadcastOn(): array
{
$channel = app()->environment('production') ? 'global-chat' : app()->environment().'.global-chat';

return [
new Channel('global-chat'),
new Channel($channel),
];
}

Expand Down
4 changes: 3 additions & 1 deletion app/Events/ChatSettingsUpdated.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@ public function __construct()
*/
public function broadcastOn(): array
{
$channel = app()->environment('production') ? 'global-chat' : app()->environment().'.global-chat';

return [
new Channel('global-chat'),
new Channel($channel),
];
}

Expand Down
17 changes: 13 additions & 4 deletions app/Http/Controllers/ChatController.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ public function index(Request $request)

$allowedEmojis = (array) AppSetting::get('global_chat_allowed_emojis', ['👍', '❤️', '🔥', '😂', '🎉', '😮', '😢', '👏']);

$channelName = app()->environment('production') ? 'global-chat' : app()->environment().'.global-chat';

if (! $request->wantsJson() && ! $request->is('api/*')) {
return Inertia::render('Chat/Index', [
'chatState' => [
Expand All @@ -92,6 +94,7 @@ public function index(Request $request)
'can_delete' => (bool) $user?->can('manage chat'),
'reaction_emojis' => $allowedEmojis,
'messages' => $messages,
'channel_name' => $channelName,
'pusher_key' => config('broadcasting.connections.pusher.key'),
'pusher_cluster' => config('broadcasting.connections.pusher.options.cluster', 'ap2'),
],
Expand All @@ -109,6 +112,7 @@ public function index(Request $request)
'can_delete' => (bool) $user?->can('manage chat'),
'reaction_emojis' => $allowedEmojis,
'messages' => $messages,
'channel_name' => $channelName,
'pusher_key' => config('broadcasting.connections.pusher.key'),
'pusher_cluster' => config('broadcasting.connections.pusher.options.cluster', 'ap2'),
]);
Expand Down Expand Up @@ -362,14 +366,19 @@ public function toggleReaction(Request $request, ChatMessage $message)
], 422);
}

$existing = $message->reactions()
$existingReaction = $message->reactions()
->where('user_id', $user->id)
->where('emoji', $emoji)
->first();

if ($existing) {
$existing->delete();
if ($existingReaction && $existingReaction->emoji === $emoji) {
// Same emoji clicked again -> toggle off / remove
$existingReaction->delete();
} else {
// Different emoji or no previous reaction -> remove existing and add new
if ($existingReaction) {
$existingReaction->delete();
}

$message->reactions()->create([
'user_id' => $user->id,
'emoji' => $emoji,
Expand Down
1 change: 1 addition & 0 deletions app/Http/Requests/User/StoreUserRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public function rules(): array
return [

'name' => ['required', 'string', 'max:255'],
'username' => ['nullable', 'string', 'min:3', 'max:30', 'regex:/^[a-zA-Z0-9_]+$/', 'unique:users,username'],
'email' => ['required', 'email', 'unique:users,email'],
'role' => ['nullable', 'string'],
'permissions' => ['nullable', 'array'],
Expand Down
9 changes: 9 additions & 0 deletions app/Http/Requests/User/UpdateUserRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ public function rules(): array

$rules = [
'name' => ['sometimes', 'string', 'max:255'],
'username' => [
'sometimes',
'nullable',
'string',
'min:3',
'max:30',
'regex:/^[a-zA-Z0-9_]+$/',
'unique:users,username,'.$user->id,
],
'email' => ['sometimes', 'email', 'unique:users,email,'.$user->id],
'file' => ['sometimes', 'nullable', 'image', 'max:2048'],
'about' => ['sometimes', 'nullable', 'string'],
Expand Down
18 changes: 10 additions & 8 deletions resources/js/components/ChatBanModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@ const banUntilInput = ref('');
const isSubmittingBan = ref(false);

const banPresets = [
{ label: '1 Hour', hours: 1 },
{ label: '24 Hours', hours: 24 },
{ label: '3 Days', hours: 72 },
{ label: '7 Days', hours: 168 },
{ label: '30 Days', hours: 720 },
{ label: '3 Mins', minutes: 3 },
{ label: '5 Mins', minutes: 5 },
{ label: '15 Mins', minutes: 15 },
{ label: '1 Hour', minutes: 60 },
{ label: '24 Hours', minutes: 24 * 60 },
{ label: '3 Days', minutes: 3 * 24 * 60 },
{ label: '7 Days', minutes: 7 * 24 * 60 },
];

const formatForDatetimeLocal = (dateString?: string | null) => {
Expand Down Expand Up @@ -70,8 +72,8 @@ const formatDate = (isoString?: string | null) => {
}
};

const applyBanPreset = (hours: number) => {
const futureDate = new Date(Date.now() + hours * 60 * 60 * 1000);
const applyBanPreset = (minutes: number) => {
const futureDate = new Date(Date.now() + minutes * 60 * 1000);
banUntilInput.value = formatForDatetimeLocal(futureDate.toISOString());
};

Expand Down Expand Up @@ -209,7 +211,7 @@ const submitBan = () => {
v-for="preset in banPresets"
:key="preset.label"
type="button"
@click="applyBanPreset(preset.hours)"
@click="applyBanPreset(preset.minutes)"
class="cursor-pointer rounded-lg border border-slate-200 bg-slate-50 px-2.5 py-1 text-xs font-semibold text-slate-700 transition hover:border-rose-200 hover:bg-rose-50 hover:text-rose-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-rose-950/40 dark:hover:text-rose-400"
>
+{{ preset.label }}
Expand Down
24 changes: 0 additions & 24 deletions resources/js/components/Footer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -75,17 +75,6 @@ const isFullFooter = computed(() => {
<Instagram class="h-5 w-5" />
</a>
</div>

<p
class="mt-4 hidden text-xs font-medium text-slate-400 md:block dark:text-gray-500"
>
A concern of
<Link
href="/u/trtajim"
class="text-slate-600 underline transition-colors hover:text-indigo-600 dark:text-gray-300 dark:hover:text-indigo-400"
>Tajim</Link
>
</p>
</div>

<div
Expand Down Expand Up @@ -195,19 +184,6 @@ const isFullFooter = computed(() => {
<div
class="mt-12 flex flex-col items-center justify-between gap-4 border-t border-slate-100 pt-8 sm:flex-row dark:border-gray-800"
>
<p
class="text-center text-xs font-medium text-slate-400 md:hidden dark:text-gray-500"
>
A concern of
<a
href="https://tajimz.xyz"
target="_blank"
rel="noopener noreferrer"
class="text-slate-600 underline transition-colors hover:text-indigo-600 dark:text-gray-300 dark:hover:text-indigo-400"
>Tajim</a
>
</p>

<p
class="text-center text-xs font-medium text-slate-400 sm:text-left dark:text-gray-500"
>
Expand Down
60 changes: 51 additions & 9 deletions resources/js/components/admin/UserRow.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<script setup lang="ts">
import { Link, router, usePage } from '@inertiajs/vue3';
import { LogIn, Pencil, Trash2, Ban } from 'lucide-vue-next';
import { ref } from 'vue';
import ChatBanModal from '@/components/ChatBanModal.vue';
import { usePermissions } from '@/lib/usePermissions';

const { can } = usePermissions();
Expand All @@ -11,6 +13,7 @@ defineProps({

const page = usePage();
const userId = page.props.auth.user.id;
const isBanModalOpen = ref(false);

const isChatBanned = (user: any) => {
if (!user?.chat_banned_until) {
Expand All @@ -20,6 +23,10 @@ const isChatBanned = (user: any) => {
return new Date(user.chat_banned_until).getTime() > Date.now();
};

const openBanModal = () => {
isBanModalOpen.value = true;
};

const getRoleBadgeStyles = (role: string) => {
switch (role) {
case 'admin':
Expand Down Expand Up @@ -59,22 +66,30 @@ const deleteUser = (id: number) => {
: '',
]"
>
<!-- Left: User Avatar + Name + Email + Role -->
<div class="flex min-w-0 flex-1 items-center gap-3">
<!-- Left: User Avatar + Name + Email + Role (Clickable to profile) -->
<Link
:href="user.username ? `/u/${user.username}` : '#'"
class="flex min-w-0 flex-1 cursor-pointer items-center gap-3"
>
<div
class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border border-black/5 bg-slate-100 text-xs font-bold text-slate-700 uppercase sm:h-10 sm:w-10 dark:border-white/10 dark:bg-gray-800 dark:text-gray-300"
class="flex h-9 w-9 shrink-0 items-center justify-center overflow-hidden rounded-lg border border-black/5 bg-slate-100 text-xs font-bold text-slate-700 uppercase sm:h-10 sm:w-10 dark:border-white/10 dark:bg-gray-800 dark:text-gray-300"
>
{{ user.name.charAt(0) }}
<img
v-if="user.image_url || user.image_path"
:src="user.image_url || '/storage/' + user.image_path"
:alt="user.name"
class="h-full w-full object-cover"
/>
<span v-else>{{ user.name.charAt(0) }}</span>
</div>

<div class="flex min-w-0 flex-col">
<div class="flex flex-wrap items-center gap-2">
<Link
:href="user.username ? `/u/${user.username}` : '#'"
class="text-sm font-semibold break-words text-slate-900 transition-colors hover:text-indigo-600 dark:text-gray-100 dark:hover:text-indigo-400"
<span
class="text-sm font-semibold break-words text-slate-900 transition-colors group-hover:text-indigo-600 dark:text-gray-100 dark:group-hover:text-indigo-400"
>
{{ user.name }}
</Link>
</span>

<span
v-if="user.id === userId"
Expand Down Expand Up @@ -106,13 +121,14 @@ const deleteUser = (id: number) => {
{{ user.email }}
</p>
</div>
</div>
</Link>

<!-- Right: Actions -->
<div
v-if="
(user.id !== userId && can('impersonate users')) ||
can('edit users') ||
can('manage chat') ||
(user.id !== userId && can('delete users'))
"
class="flex shrink-0 items-center gap-1"
Expand All @@ -128,6 +144,25 @@ const deleteUser = (id: number) => {
<LogIn class="h-4 w-4" :stroke-width="1.8" />
</button>

<button
v-if="can('manage chat') || can('edit users')"
type="button"
@click="openBanModal"
class="rounded-lg p-1.5 transition-colors"
:class="
isChatBanned(user)
? 'text-rose-600 hover:bg-rose-50 dark:text-rose-400 dark:hover:bg-rose-950/40'
: 'text-slate-400 hover:bg-rose-50 hover:text-rose-600 dark:text-gray-500 dark:hover:bg-rose-950/40 dark:hover:text-rose-400'
"
:title="
isChatBanned(user)
? 'Edit chat ban timer / Unban'
: 'Ban from chat'
"
>
<Ban class="h-4 w-4" :stroke-width="1.8" />
</button>

<Link
v-if="can('edit users')"
:href="`/admin/users/edit/${user.id}`"
Expand All @@ -147,5 +182,12 @@ const deleteUser = (id: number) => {
<Trash2 class="h-4 w-4" :stroke-width="1.8" />
</button>
</div>

<!-- Chat Ban Modal -->
<ChatBanModal
:is-open="isBanModalOpen"
:user="user"
@close="isBanModalOpen = false"
/>
</div>
</template>
Loading