mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-11 20:05:19 +00:00
feat/enh: pinned messages in channels
This commit is contained in:
parent
451907cc92
commit
aae2fce173
7 changed files with 510 additions and 58 deletions
|
|
@ -111,6 +111,10 @@ class MessageReplyToResponse(MessageUserResponse):
|
|||
reply_to_message: Optional[MessageUserResponse] = None
|
||||
|
||||
|
||||
class MessageWithReactionsResponse(MessageUserResponse):
|
||||
reactions: list[Reactions]
|
||||
|
||||
|
||||
class MessageResponse(MessageReplyToResponse):
|
||||
latest_reply_at: Optional[int]
|
||||
reply_count: int
|
||||
|
|
@ -306,6 +310,20 @@ class MessageTable:
|
|||
)
|
||||
return MessageModel.model_validate(message) if message else None
|
||||
|
||||
def get_pinned_messages_by_channel_id(
|
||||
self, channel_id: str, skip: int = 0, limit: int = 50
|
||||
) -> list[MessageModel]:
|
||||
with get_db() as db:
|
||||
all_messages = (
|
||||
db.query(Message)
|
||||
.filter_by(channel_id=channel_id, is_pinned=True)
|
||||
.order_by(Message.pinned_at.desc())
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
return [MessageModel.model_validate(message) for message in all_messages]
|
||||
|
||||
def update_message_by_id(
|
||||
self, id: str, form_data: MessageForm
|
||||
) -> Optional[MessageModel]:
|
||||
|
|
@ -325,7 +343,7 @@ class MessageTable:
|
|||
db.refresh(message)
|
||||
return MessageModel.model_validate(message) if message else None
|
||||
|
||||
def update_message_pin_by_id(
|
||||
def update_is_pinned_by_id(
|
||||
self, id: str, is_pinned: bool, pinned_by: Optional[str] = None
|
||||
) -> Optional[MessageModel]:
|
||||
with get_db() as db:
|
||||
|
|
@ -333,7 +351,6 @@ class MessageTable:
|
|||
message.is_pinned = is_pinned
|
||||
message.pinned_at = int(time.time_ns()) if is_pinned else None
|
||||
message.pinned_by = pinned_by if is_pinned else None
|
||||
message.updated_at = int(time.time_ns())
|
||||
db.commit()
|
||||
db.refresh(message)
|
||||
return MessageModel.model_validate(message) if message else None
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ from open_webui.models.messages import (
|
|||
Messages,
|
||||
MessageModel,
|
||||
MessageResponse,
|
||||
MessageWithReactionsResponse,
|
||||
MessageForm,
|
||||
)
|
||||
|
||||
|
|
@ -463,6 +464,62 @@ async def get_channel_messages(
|
|||
return messages
|
||||
|
||||
|
||||
############################
|
||||
# GetPinnedChannelMessages
|
||||
############################
|
||||
|
||||
PAGE_ITEM_COUNT_PINNED = 20
|
||||
|
||||
|
||||
@router.get("/{id}/messages/pinned", response_model=list[MessageWithReactionsResponse])
|
||||
async def get_pinned_channel_messages(
|
||||
id: str, page: int = 1, user=Depends(get_verified_user)
|
||||
):
|
||||
channel = Channels.get_channel_by_id(id)
|
||||
if not channel:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
|
||||
)
|
||||
|
||||
if channel.type == "dm":
|
||||
if not Channels.is_user_channel_member(channel.id, user.id):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
||||
)
|
||||
else:
|
||||
if user.role != "admin" and not has_access(
|
||||
user.id, type="read", access_control=channel.access_control
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
||||
)
|
||||
|
||||
page = max(1, page)
|
||||
skip = (page - 1) * PAGE_ITEM_COUNT_PINNED
|
||||
limit = PAGE_ITEM_COUNT_PINNED
|
||||
|
||||
message_list = Messages.get_pinned_messages_by_channel_id(id, skip, limit)
|
||||
users = {}
|
||||
|
||||
messages = []
|
||||
for message in message_list:
|
||||
if message.user_id not in users:
|
||||
user = Users.get_user_by_id(message.user_id)
|
||||
users[message.user_id] = user
|
||||
|
||||
messages.append(
|
||||
MessageWithReactionsResponse(
|
||||
**{
|
||||
**message.model_dump(),
|
||||
"reactions": Messages.get_reactions_by_message_id(message.id),
|
||||
"user": UserNameResponse(**users[message.user_id].model_dump()),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
return messages
|
||||
|
||||
|
||||
############################
|
||||
# PostNewMessage
|
||||
############################
|
||||
|
|
@ -834,6 +891,69 @@ async def get_channel_message(
|
|||
)
|
||||
|
||||
|
||||
############################
|
||||
# PinChannelMessage
|
||||
############################
|
||||
|
||||
|
||||
class PinMessageForm(BaseModel):
|
||||
is_pinned: bool
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{id}/messages/{message_id}/pin", response_model=Optional[MessageUserResponse]
|
||||
)
|
||||
async def pin_channel_message(
|
||||
id: str, message_id: str, form_data: PinMessageForm, user=Depends(get_verified_user)
|
||||
):
|
||||
channel = Channels.get_channel_by_id(id)
|
||||
if not channel:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
|
||||
)
|
||||
|
||||
if channel.type == "dm":
|
||||
if not Channels.is_user_channel_member(channel.id, user.id):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
||||
)
|
||||
else:
|
||||
if user.role != "admin" and not has_access(
|
||||
user.id, type="read", access_control=channel.access_control
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
||||
)
|
||||
|
||||
message = Messages.get_message_by_id(message_id)
|
||||
if not message:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
|
||||
)
|
||||
|
||||
if message.channel_id != id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
|
||||
)
|
||||
|
||||
try:
|
||||
Messages.update_is_pinned_by_id(message_id, form_data.is_pinned, user.id)
|
||||
message = Messages.get_message_by_id(message_id)
|
||||
return MessageUserResponse(
|
||||
**{
|
||||
**message.model_dump(),
|
||||
"user": UserNameResponse(
|
||||
**Users.get_user_by_id(message.user_id).model_dump()
|
||||
),
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# GetChannelThreadMessages
|
||||
############################
|
||||
|
|
|
|||
|
|
@ -299,6 +299,44 @@ export const getChannelMessages = async (
|
|||
return res;
|
||||
};
|
||||
|
||||
export const getChannelPinnedMessages = async (
|
||||
token: string = '',
|
||||
channel_id: string,
|
||||
page: number = 1
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(
|
||||
`${WEBUI_API_BASE_URL}/channels/${channel_id}/messages/pinned?page=${page}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
)
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
console.error(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getChannelThreadMessages = async (
|
||||
token: string = '',
|
||||
channel_id: string,
|
||||
|
|
@ -379,6 +417,46 @@ export const sendMessage = async (token: string = '', channel_id: string, messag
|
|||
return res;
|
||||
};
|
||||
|
||||
export const pinMessage = async (
|
||||
token: string = '',
|
||||
channel_id: string,
|
||||
message_id: string,
|
||||
is_pinned: boolean
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(
|
||||
`${WEBUI_API_BASE_URL}/channels/${channel_id}/messages/${message_id}/pin`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({ is_pinned })
|
||||
}
|
||||
)
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
console.error(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const updateMessage = async (
|
||||
token: string = '',
|
||||
channel_id: string,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,13 @@
|
|||
import Message from './Messages/Message.svelte';
|
||||
import Loader from '../common/Loader.svelte';
|
||||
import Spinner from '../common/Spinner.svelte';
|
||||
import { addReaction, deleteMessage, removeReaction, updateMessage } from '$lib/apis/channels';
|
||||
import {
|
||||
addReaction,
|
||||
deleteMessage,
|
||||
pinMessage,
|
||||
removeReaction,
|
||||
updateMessage
|
||||
} from '$lib/apis/channels';
|
||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
|
@ -155,6 +161,26 @@
|
|||
onReply={(message) => {
|
||||
onReply(message);
|
||||
}}
|
||||
onPin={async (message) => {
|
||||
messages = messages.map((m) => {
|
||||
if (m.id === message.id) {
|
||||
m.is_pinned = !m.is_pinned;
|
||||
m.pinned_by = !m.is_pinned ? null : $user?.id;
|
||||
m.pinned_at = !m.is_pinned ? null : Date.now() * 1000000;
|
||||
}
|
||||
return m;
|
||||
});
|
||||
|
||||
const updatedMessage = await pinMessage(
|
||||
localStorage.token,
|
||||
message.channel_id,
|
||||
message.id,
|
||||
message.is_pinned
|
||||
).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
return null;
|
||||
});
|
||||
}}
|
||||
onThread={(id) => {
|
||||
onThread(id);
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,10 @@
|
|||
import Emoji from '$lib/components/common/Emoji.svelte';
|
||||
import Skeleton from '$lib/components/chat/Messages/Skeleton.svelte';
|
||||
import ArrowUpLeftAlt from '$lib/components/icons/ArrowUpLeftAlt.svelte';
|
||||
import PinSlash from '$lib/components/icons/PinSlash.svelte';
|
||||
import Pin from '$lib/components/icons/Pin.svelte';
|
||||
|
||||
export let className = '';
|
||||
|
||||
export let message;
|
||||
export let showUserProfile = true;
|
||||
|
|
@ -47,6 +51,7 @@
|
|||
export let onDelete: Function = () => {};
|
||||
export let onEdit: Function = () => {};
|
||||
export let onReply: Function = () => {};
|
||||
export let onPin: Function = () => {};
|
||||
export let onThread: Function = () => {};
|
||||
export let onReaction: Function = () => {};
|
||||
|
||||
|
|
@ -69,13 +74,17 @@
|
|||
{#if message}
|
||||
<div
|
||||
id="message-{message.id}"
|
||||
class="flex flex-col justify-between px-5 {showUserProfile
|
||||
class="flex flex-col justify-between w-full max-w-full mx-auto group hover:bg-gray-300/5 dark:hover:bg-gray-700/5 transition relative {className
|
||||
? className
|
||||
: `px-5 ${
|
||||
replyToMessage ? 'border-l-4 border-blue-500 bg-blue-100/10 dark:bg-blue-100/5 pl-4' : ''
|
||||
} ${
|
||||
(message?.reply_to_message?.meta?.model_id ?? message?.reply_to_message?.user_id) ===
|
||||
$user?.id
|
||||
? 'border-l-4 border-orange-500 bg-orange-100/10 dark:bg-orange-100/5 pl-4'
|
||||
: ''
|
||||
} ${message?.is_pinned ? 'bg-yellow-100/20 dark:bg-yellow-100/5' : ''}`} {showUserProfile
|
||||
? 'pt-1.5 pb-0.5'
|
||||
: ''} w-full max-w-full mx-auto group hover:bg-gray-300/5 dark:hover:bg-gray-700/5 transition relative {replyToMessage
|
||||
? 'border-l-4 border-blue-500 bg-blue-100/10 dark:bg-blue-100/5 pl-4'
|
||||
: ''} {(message?.reply_to_message?.meta?.model_id ?? message?.reply_to_message?.user_id) ===
|
||||
$user?.id
|
||||
? 'border-l-4 border-orange-500 bg-orange-100/10 dark:bg-orange-100/5 pl-4'
|
||||
: ''}"
|
||||
>
|
||||
{#if !edit && !disabled}
|
||||
|
|
@ -85,37 +94,56 @@
|
|||
<div
|
||||
class="flex gap-1 rounded-lg bg-white dark:bg-gray-850 shadow-md p-0.5 border border-gray-100 dark:border-gray-850"
|
||||
>
|
||||
<EmojiPicker
|
||||
onClose={() => (showButtons = false)}
|
||||
onSubmit={(name) => {
|
||||
showButtons = false;
|
||||
onReaction(name);
|
||||
}}
|
||||
>
|
||||
<Tooltip content={$i18n.t('Add Reaction')}>
|
||||
<button
|
||||
class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-1"
|
||||
on:click={() => {
|
||||
showButtons = true;
|
||||
}}
|
||||
>
|
||||
<FaceSmile />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</EmojiPicker>
|
||||
|
||||
<Tooltip content={$i18n.t('Reply')}>
|
||||
<button
|
||||
class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-0.5"
|
||||
on:click={() => {
|
||||
onReply(message);
|
||||
{#if onReaction}
|
||||
<EmojiPicker
|
||||
onClose={() => (showButtons = false)}
|
||||
onSubmit={(name) => {
|
||||
showButtons = false;
|
||||
onReaction(name);
|
||||
}}
|
||||
>
|
||||
<ArrowUpLeftAlt className="size-5" />
|
||||
<Tooltip content={$i18n.t('Add Reaction')}>
|
||||
<button
|
||||
class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-1"
|
||||
on:click={() => {
|
||||
showButtons = true;
|
||||
}}
|
||||
>
|
||||
<FaceSmile />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</EmojiPicker>
|
||||
{/if}
|
||||
|
||||
{#if onReply}
|
||||
<Tooltip content={$i18n.t('Reply')}>
|
||||
<button
|
||||
class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-0.5"
|
||||
on:click={() => {
|
||||
onReply(message);
|
||||
}}
|
||||
>
|
||||
<ArrowUpLeftAlt className="size-5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
<Tooltip content={message?.is_pinned ? $i18n.t('Unpin') : $i18n.t('Pin')}>
|
||||
<button
|
||||
class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-1"
|
||||
on:click={() => {
|
||||
onPin(message);
|
||||
}}
|
||||
>
|
||||
{#if message?.is_pinned}
|
||||
<PinSlash className="size-4" />
|
||||
{:else}
|
||||
<Pin className="size-4" />
|
||||
{/if}
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
{#if !thread}
|
||||
{#if !thread && onThread}
|
||||
<Tooltip content={$i18n.t('Reply in Thread')}>
|
||||
<button
|
||||
class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-1"
|
||||
|
|
@ -129,31 +157,44 @@
|
|||
{/if}
|
||||
|
||||
{#if message.user_id === $user?.id || $user?.role === 'admin'}
|
||||
<Tooltip content={$i18n.t('Edit')}>
|
||||
<button
|
||||
class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-1"
|
||||
on:click={() => {
|
||||
edit = true;
|
||||
editedContent = message.content;
|
||||
}}
|
||||
>
|
||||
<Pencil />
|
||||
</button>
|
||||
</Tooltip>
|
||||
{#if onEdit}
|
||||
<Tooltip content={$i18n.t('Edit')}>
|
||||
<button
|
||||
class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-1"
|
||||
on:click={() => {
|
||||
edit = true;
|
||||
editedContent = message.content;
|
||||
}}
|
||||
>
|
||||
<Pencil />
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
<Tooltip content={$i18n.t('Delete')}>
|
||||
<button
|
||||
class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-1"
|
||||
on:click={() => (showDeleteConfirmDialog = true)}
|
||||
>
|
||||
<GarbageBin />
|
||||
</button>
|
||||
</Tooltip>
|
||||
{#if onDelete}
|
||||
<Tooltip content={$i18n.t('Delete')}>
|
||||
<button
|
||||
class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-1"
|
||||
on:click={() => (showDeleteConfirmDialog = true)}
|
||||
>
|
||||
<GarbageBin />
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if message?.is_pinned}
|
||||
<div class="flex {showUserProfile ? 'mb-0.5' : 'mt-0.5'}">
|
||||
<div class="ml-8.5 flex items-center gap-1 px-1 rounded-full text-xs">
|
||||
<Pin className="size-3 text-yellow-500 dark:text-yellow-300" />
|
||||
<span class="text-gray-500">{$i18n.t('Pinned')}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if message?.reply_to_message?.user}
|
||||
<div class="relative text-xs mb-1">
|
||||
<div
|
||||
|
|
@ -203,12 +244,13 @@
|
|||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class=" flex w-full message-{message.id}"
|
||||
class=" flex w-full message-{message.id} "
|
||||
id="message-{message.id}"
|
||||
dir={$settings.chatDirection}
|
||||
>
|
||||
<div class={`shrink-0 mr-3 w-9`}>
|
||||
<div class={`shrink-0 mr-1 w-9`}>
|
||||
{#if showUserProfile}
|
||||
{#if message?.meta?.model_id}
|
||||
<img
|
||||
|
|
@ -239,7 +281,7 @@
|
|||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex-auto w-0 pl-1">
|
||||
<div class="flex-auto w-0 pl-2">
|
||||
{#if showUserProfile}
|
||||
<Name>
|
||||
<div class=" self-end text-base shrink-0 font-medium truncate">
|
||||
|
|
|
|||
|
|
@ -18,16 +18,20 @@
|
|||
import UserAlt from '../icons/UserAlt.svelte';
|
||||
import ChannelInfoModal from './ChannelInfoModal.svelte';
|
||||
import Users from '../icons/Users.svelte';
|
||||
import Pin from '../icons/Pin.svelte';
|
||||
import PinnedMessagesModal from './PinnedMessagesModal.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
let showChannelPinnedMessagesModal = false;
|
||||
let showChannelInfoModal = false;
|
||||
|
||||
export let channel;
|
||||
</script>
|
||||
|
||||
<PinnedMessagesModal bind:show={showChannelPinnedMessagesModal} {channel} />
|
||||
<ChannelInfoModal bind:show={showChannelInfoModal} {channel} />
|
||||
<nav class="sticky top-0 z-30 w-full px-1.5 py-1 -mb-8 flex items-center drag-region">
|
||||
<nav class="sticky top-0 z-30 w-full px-1.5 py-1 -mb-8 flex items-center drag-region flex flex-col">
|
||||
<div
|
||||
id="navbar-bg-gradient-to-b"
|
||||
class=" bg-linear-to-b via-50% from-white via-white to-transparent dark:from-gray-900 dark:via-gray-900 dark:to-transparent pointer-events-none absolute inset-0 -bottom-7 z-[-1]"
|
||||
|
|
@ -111,6 +115,21 @@
|
|||
</div>
|
||||
|
||||
<div class="self-start flex flex-none items-center text-gray-600 dark:text-gray-400 gap-1">
|
||||
<Tooltip content={$i18n.t('Pinned Messages')}>
|
||||
<button
|
||||
class=" flex cursor-pointer py-1.5 px-1.5 border dark:border-gray-850 border-gray-50 rounded-xl text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-850 transition"
|
||||
aria-label="Pinned Messages"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
showChannelPinnedMessagesModal = true;
|
||||
}}
|
||||
>
|
||||
<div class=" flex items-center gap-0.5 m-auto self-center">
|
||||
<Pin className=" size-4" strokeWidth="1.5" />
|
||||
</div>
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
{#if channel?.user_count !== undefined}
|
||||
<Tooltip content={$i18n.t('Users')}>
|
||||
<button
|
||||
|
|
|
|||
150
src/lib/components/channel/PinnedMessagesModal.svelte
Normal file
150
src/lib/components/channel/PinnedMessagesModal.svelte
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { getContext, onMount } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
|
||||
import UserPlusSolid from '$lib/components/icons/UserPlusSolid.svelte';
|
||||
import WrenchSolid from '$lib/components/icons/WrenchSolid.svelte';
|
||||
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
import Hashtag from '../icons/Hashtag.svelte';
|
||||
import Lock from '../icons/Lock.svelte';
|
||||
import UserList from './ChannelInfoModal/UserList.svelte';
|
||||
import { getChannelPinnedMessages, pinMessage } from '$lib/apis/channels';
|
||||
import Message from './Messages/Message.svelte';
|
||||
import { user } from '$lib/stores';
|
||||
import Loader from '../common/Loader.svelte';
|
||||
|
||||
export let show = false;
|
||||
export let channel = null;
|
||||
|
||||
let page = 1;
|
||||
let pinnedMessages = [];
|
||||
|
||||
let allItemsLoaded = false;
|
||||
let loading = false;
|
||||
|
||||
const getPinnedMessages = async () => {
|
||||
if (!channel) return;
|
||||
if (allItemsLoaded) return;
|
||||
|
||||
loading = true;
|
||||
try {
|
||||
const res = await getChannelPinnedMessages(localStorage.token, channel.id, page).catch(
|
||||
(error) => {
|
||||
toast.error(`${error}`);
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
if (res) {
|
||||
pinnedMessages = [...pinnedMessages, ...res];
|
||||
}
|
||||
|
||||
if (res.length === 0) {
|
||||
allItemsLoaded = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching pinned messages:', error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
const init = () => {
|
||||
page = 1;
|
||||
pinnedMessages = [];
|
||||
|
||||
getPinnedMessages();
|
||||
};
|
||||
|
||||
$: if (show) {
|
||||
init();
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
init();
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if channel}
|
||||
<Modal size="sm" bind:show>
|
||||
<div>
|
||||
<div class=" flex justify-between dark:text-gray-100 px-5 pt-4 mb-1.5">
|
||||
<div class="self-center text-base">
|
||||
<div class="flex items-center gap-0.5 shrink-0">
|
||||
{$i18n.t('Pinned Messages')}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="self-center"
|
||||
on:click={() => {
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col md:flex-row w-full px-4 pb-4 md:space-x-4 dark:text-gray-200">
|
||||
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
||||
<div class="flex flex-col w-full h-full pb-2 gap-1">
|
||||
<div
|
||||
class="flex flex-col gap-2 max-h-[60vh] overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 dark:scrollbar-thumb-gray-700 scrollbar-track-transparent py-2"
|
||||
>
|
||||
{#each pinnedMessages as message, messageIdx (message.id)}
|
||||
<Message
|
||||
className="rounded-xl px-2"
|
||||
{message}
|
||||
{channel}
|
||||
onPin={async (message) => {
|
||||
pinnedMessages = pinnedMessages.filter((m) => m.id !== message.id);
|
||||
|
||||
const updatedMessage = await pinMessage(
|
||||
localStorage.token,
|
||||
message.channel_id,
|
||||
message.id,
|
||||
!message.is_pinned
|
||||
).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
return null;
|
||||
});
|
||||
|
||||
init();
|
||||
}}
|
||||
onReaction={false}
|
||||
onThread={false}
|
||||
onReply={false}
|
||||
onEdit={false}
|
||||
onDelete={false}
|
||||
/>
|
||||
|
||||
{#if messageIdx === pinnedMessages.length - 1 && !allItemsLoaded}
|
||||
<Loader
|
||||
on:visible={(e) => {
|
||||
console.log('visible');
|
||||
if (!loading) {
|
||||
page += 1;
|
||||
getPinnedMessages();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="w-full flex justify-center py-1 text-xs animate-pulse items-center gap-2"
|
||||
>
|
||||
<Spinner className=" size-4" />
|
||||
<div class=" ">{$i18n.t('Loading...')}</div>
|
||||
</div>
|
||||
</Loader>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
{/if}
|
||||
Loading…
Reference in a new issue