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
|
reply_to_message: Optional[MessageUserResponse] = None
|
||||||
|
|
||||||
|
|
||||||
|
class MessageWithReactionsResponse(MessageUserResponse):
|
||||||
|
reactions: list[Reactions]
|
||||||
|
|
||||||
|
|
||||||
class MessageResponse(MessageReplyToResponse):
|
class MessageResponse(MessageReplyToResponse):
|
||||||
latest_reply_at: Optional[int]
|
latest_reply_at: Optional[int]
|
||||||
reply_count: int
|
reply_count: int
|
||||||
|
|
@ -306,6 +310,20 @@ class MessageTable:
|
||||||
)
|
)
|
||||||
return MessageModel.model_validate(message) if message else None
|
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(
|
def update_message_by_id(
|
||||||
self, id: str, form_data: MessageForm
|
self, id: str, form_data: MessageForm
|
||||||
) -> Optional[MessageModel]:
|
) -> Optional[MessageModel]:
|
||||||
|
|
@ -325,7 +343,7 @@ class MessageTable:
|
||||||
db.refresh(message)
|
db.refresh(message)
|
||||||
return MessageModel.model_validate(message) if message else None
|
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
|
self, id: str, is_pinned: bool, pinned_by: Optional[str] = None
|
||||||
) -> Optional[MessageModel]:
|
) -> Optional[MessageModel]:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
|
|
@ -333,7 +351,6 @@ class MessageTable:
|
||||||
message.is_pinned = is_pinned
|
message.is_pinned = is_pinned
|
||||||
message.pinned_at = int(time.time_ns()) if is_pinned else None
|
message.pinned_at = int(time.time_ns()) if is_pinned else None
|
||||||
message.pinned_by = pinned_by 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.commit()
|
||||||
db.refresh(message)
|
db.refresh(message)
|
||||||
return MessageModel.model_validate(message) if message else None
|
return MessageModel.model_validate(message) if message else None
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ from open_webui.models.messages import (
|
||||||
Messages,
|
Messages,
|
||||||
MessageModel,
|
MessageModel,
|
||||||
MessageResponse,
|
MessageResponse,
|
||||||
|
MessageWithReactionsResponse,
|
||||||
MessageForm,
|
MessageForm,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -463,6 +464,62 @@ async def get_channel_messages(
|
||||||
return 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
|
# 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
|
# GetChannelThreadMessages
|
||||||
############################
|
############################
|
||||||
|
|
|
||||||
|
|
@ -299,6 +299,44 @@ export const getChannelMessages = async (
|
||||||
return res;
|
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 (
|
export const getChannelThreadMessages = async (
|
||||||
token: string = '',
|
token: string = '',
|
||||||
channel_id: string,
|
channel_id: string,
|
||||||
|
|
@ -379,6 +417,46 @@ export const sendMessage = async (token: string = '', channel_id: string, messag
|
||||||
return res;
|
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 (
|
export const updateMessage = async (
|
||||||
token: string = '',
|
token: string = '',
|
||||||
channel_id: string,
|
channel_id: string,
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,13 @@
|
||||||
import Message from './Messages/Message.svelte';
|
import Message from './Messages/Message.svelte';
|
||||||
import Loader from '../common/Loader.svelte';
|
import Loader from '../common/Loader.svelte';
|
||||||
import Spinner from '../common/Spinner.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';
|
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
@ -155,6 +161,26 @@
|
||||||
onReply={(message) => {
|
onReply={(message) => {
|
||||||
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) => {
|
||||||
onThread(id);
|
onThread(id);
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,10 @@
|
||||||
import Emoji from '$lib/components/common/Emoji.svelte';
|
import Emoji from '$lib/components/common/Emoji.svelte';
|
||||||
import Skeleton from '$lib/components/chat/Messages/Skeleton.svelte';
|
import Skeleton from '$lib/components/chat/Messages/Skeleton.svelte';
|
||||||
import ArrowUpLeftAlt from '$lib/components/icons/ArrowUpLeftAlt.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 message;
|
||||||
export let showUserProfile = true;
|
export let showUserProfile = true;
|
||||||
|
|
@ -47,6 +51,7 @@
|
||||||
export let onDelete: Function = () => {};
|
export let onDelete: Function = () => {};
|
||||||
export let onEdit: Function = () => {};
|
export let onEdit: Function = () => {};
|
||||||
export let onReply: Function = () => {};
|
export let onReply: Function = () => {};
|
||||||
|
export let onPin: Function = () => {};
|
||||||
export let onThread: Function = () => {};
|
export let onThread: Function = () => {};
|
||||||
export let onReaction: Function = () => {};
|
export let onReaction: Function = () => {};
|
||||||
|
|
||||||
|
|
@ -69,13 +74,17 @@
|
||||||
{#if message}
|
{#if message}
|
||||||
<div
|
<div
|
||||||
id="message-{message.id}"
|
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
|
||||||
? 'pt-1.5 pb-0.5'
|
? className
|
||||||
: ''} w-full max-w-full mx-auto group hover:bg-gray-300/5 dark:hover:bg-gray-700/5 transition relative {replyToMessage
|
: `px-5 ${
|
||||||
? 'border-l-4 border-blue-500 bg-blue-100/10 dark:bg-blue-100/5 pl-4'
|
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) ===
|
} ${
|
||||||
|
(message?.reply_to_message?.meta?.model_id ?? message?.reply_to_message?.user_id) ===
|
||||||
$user?.id
|
$user?.id
|
||||||
? 'border-l-4 border-orange-500 bg-orange-100/10 dark:bg-orange-100/5 pl-4'
|
? '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'
|
||||||
: ''}"
|
: ''}"
|
||||||
>
|
>
|
||||||
{#if !edit && !disabled}
|
{#if !edit && !disabled}
|
||||||
|
|
@ -85,6 +94,7 @@
|
||||||
<div
|
<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"
|
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"
|
||||||
>
|
>
|
||||||
|
{#if onReaction}
|
||||||
<EmojiPicker
|
<EmojiPicker
|
||||||
onClose={() => (showButtons = false)}
|
onClose={() => (showButtons = false)}
|
||||||
onSubmit={(name) => {
|
onSubmit={(name) => {
|
||||||
|
|
@ -103,7 +113,9 @@
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</EmojiPicker>
|
</EmojiPicker>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if onReply}
|
||||||
<Tooltip content={$i18n.t('Reply')}>
|
<Tooltip content={$i18n.t('Reply')}>
|
||||||
<button
|
<button
|
||||||
class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-0.5"
|
class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-0.5"
|
||||||
|
|
@ -114,8 +126,24 @@
|
||||||
<ArrowUpLeftAlt className="size-5" />
|
<ArrowUpLeftAlt className="size-5" />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if !thread}
|
<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 && onThread}
|
||||||
<Tooltip content={$i18n.t('Reply in Thread')}>
|
<Tooltip content={$i18n.t('Reply in Thread')}>
|
||||||
<button
|
<button
|
||||||
class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-1"
|
class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-1"
|
||||||
|
|
@ -129,6 +157,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if message.user_id === $user?.id || $user?.role === 'admin'}
|
{#if message.user_id === $user?.id || $user?.role === 'admin'}
|
||||||
|
{#if onEdit}
|
||||||
<Tooltip content={$i18n.t('Edit')}>
|
<Tooltip content={$i18n.t('Edit')}>
|
||||||
<button
|
<button
|
||||||
class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-1"
|
class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-1"
|
||||||
|
|
@ -140,7 +169,9 @@
|
||||||
<Pencil />
|
<Pencil />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if onDelete}
|
||||||
<Tooltip content={$i18n.t('Delete')}>
|
<Tooltip content={$i18n.t('Delete')}>
|
||||||
<button
|
<button
|
||||||
class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-1"
|
class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-1"
|
||||||
|
|
@ -150,6 +181,16 @@
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/if}
|
{/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>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -203,12 +244,13 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class=" flex w-full message-{message.id} "
|
class=" flex w-full message-{message.id} "
|
||||||
id="message-{message.id}"
|
id="message-{message.id}"
|
||||||
dir={$settings.chatDirection}
|
dir={$settings.chatDirection}
|
||||||
>
|
>
|
||||||
<div class={`shrink-0 mr-3 w-9`}>
|
<div class={`shrink-0 mr-1 w-9`}>
|
||||||
{#if showUserProfile}
|
{#if showUserProfile}
|
||||||
{#if message?.meta?.model_id}
|
{#if message?.meta?.model_id}
|
||||||
<img
|
<img
|
||||||
|
|
@ -239,7 +281,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-auto w-0 pl-1">
|
<div class="flex-auto w-0 pl-2">
|
||||||
{#if showUserProfile}
|
{#if showUserProfile}
|
||||||
<Name>
|
<Name>
|
||||||
<div class=" self-end text-base shrink-0 font-medium truncate">
|
<div class=" self-end text-base shrink-0 font-medium truncate">
|
||||||
|
|
|
||||||
|
|
@ -18,16 +18,20 @@
|
||||||
import UserAlt from '../icons/UserAlt.svelte';
|
import UserAlt from '../icons/UserAlt.svelte';
|
||||||
import ChannelInfoModal from './ChannelInfoModal.svelte';
|
import ChannelInfoModal from './ChannelInfoModal.svelte';
|
||||||
import Users from '../icons/Users.svelte';
|
import Users from '../icons/Users.svelte';
|
||||||
|
import Pin from '../icons/Pin.svelte';
|
||||||
|
import PinnedMessagesModal from './PinnedMessagesModal.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
let showChannelPinnedMessagesModal = false;
|
||||||
let showChannelInfoModal = false;
|
let showChannelInfoModal = false;
|
||||||
|
|
||||||
export let channel;
|
export let channel;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<PinnedMessagesModal bind:show={showChannelPinnedMessagesModal} {channel} />
|
||||||
<ChannelInfoModal bind:show={showChannelInfoModal} {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
|
<div
|
||||||
id="navbar-bg-gradient-to-b"
|
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]"
|
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>
|
||||||
|
|
||||||
<div class="self-start flex flex-none items-center text-gray-600 dark:text-gray-400 gap-1">
|
<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}
|
{#if channel?.user_count !== undefined}
|
||||||
<Tooltip content={$i18n.t('Users')}>
|
<Tooltip content={$i18n.t('Users')}>
|
||||||
<button
|
<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