mirror of
https://github.com/open-webui/open-webui
synced 2025-05-14 10:36:12 +00:00
feat: reactions
This commit is contained in:
parent
4b0fa112bb
commit
f93c2e4a8d
@ -0,0 +1,70 @@
|
|||||||
|
"""Update message & channel tables
|
||||||
|
|
||||||
|
Revision ID: 3781e22d8b01
|
||||||
|
Revises: 7826ab40b532
|
||||||
|
Create Date: 2024-12-30 03:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
revision = "3781e22d8b01"
|
||||||
|
down_revision = "7826ab40b532"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# Add 'type' column to the 'channel' table
|
||||||
|
op.add_column(
|
||||||
|
"channel",
|
||||||
|
sa.Column(
|
||||||
|
"type",
|
||||||
|
sa.Text(),
|
||||||
|
nullable=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add 'parent_id' column to the 'message' table for threads
|
||||||
|
op.add_column(
|
||||||
|
"message",
|
||||||
|
sa.Column("parent_id", sa.Text(), nullable=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
op.create_table(
|
||||||
|
"message_reaction",
|
||||||
|
sa.Column(
|
||||||
|
"id", sa.Text(), nullable=False, primary_key=True, unique=True
|
||||||
|
), # Unique reaction ID
|
||||||
|
sa.Column("user_id", sa.Text(), nullable=False), # User who reacted
|
||||||
|
sa.Column(
|
||||||
|
"message_id", sa.Text(), nullable=False
|
||||||
|
), # Message that was reacted to
|
||||||
|
sa.Column(
|
||||||
|
"name", sa.Text(), nullable=False
|
||||||
|
), # Reaction name (e.g. "thumbs_up")
|
||||||
|
sa.Column(
|
||||||
|
"created_at", sa.BigInteger(), nullable=True
|
||||||
|
), # Timestamp of when the reaction was added
|
||||||
|
)
|
||||||
|
|
||||||
|
op.create_table(
|
||||||
|
"channel_member",
|
||||||
|
sa.Column(
|
||||||
|
"id", sa.Text(), nullable=False, primary_key=True, unique=True
|
||||||
|
), # Record ID for the membership row
|
||||||
|
sa.Column("channel_id", sa.Text(), nullable=False), # Associated channel
|
||||||
|
sa.Column("user_id", sa.Text(), nullable=False), # Associated user
|
||||||
|
sa.Column(
|
||||||
|
"created_at", sa.BigInteger(), nullable=True
|
||||||
|
), # Timestamp of when the user joined the channel
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# Revert 'type' column addition to the 'channel' table
|
||||||
|
op.drop_column("channel", "type")
|
||||||
|
op.drop_column("message", "parent_id")
|
||||||
|
op.drop_table("message_reaction")
|
||||||
|
op.drop_table("channel_member")
|
@ -21,6 +21,7 @@ class Channel(Base):
|
|||||||
|
|
||||||
id = Column(Text, primary_key=True)
|
id = Column(Text, primary_key=True)
|
||||||
user_id = Column(Text)
|
user_id = Column(Text)
|
||||||
|
type = Column(Text, nullable=True)
|
||||||
|
|
||||||
name = Column(Text)
|
name = Column(Text)
|
||||||
description = Column(Text, nullable=True)
|
description = Column(Text, nullable=True)
|
||||||
@ -38,9 +39,11 @@ class ChannelModel(BaseModel):
|
|||||||
|
|
||||||
id: str
|
id: str
|
||||||
user_id: str
|
user_id: str
|
||||||
description: Optional[str] = None
|
type: Optional[str] = None
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
|
description: Optional[str] = None
|
||||||
|
|
||||||
data: Optional[dict] = None
|
data: Optional[dict] = None
|
||||||
meta: Optional[dict] = None
|
meta: Optional[dict] = None
|
||||||
access_control: Optional[dict] = None
|
access_control: Optional[dict] = None
|
||||||
@ -64,12 +67,13 @@ class ChannelForm(BaseModel):
|
|||||||
|
|
||||||
class ChannelTable:
|
class ChannelTable:
|
||||||
def insert_new_channel(
|
def insert_new_channel(
|
||||||
self, form_data: ChannelForm, user_id: str
|
self, type: Optional[str], form_data: ChannelForm, user_id: str
|
||||||
) -> Optional[ChannelModel]:
|
) -> Optional[ChannelModel]:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
channel = ChannelModel(
|
channel = ChannelModel(
|
||||||
**{
|
**{
|
||||||
**form_data.model_dump(),
|
**form_data.model_dump(),
|
||||||
|
"type": type,
|
||||||
"name": form_data.name.lower(),
|
"name": form_data.name.lower(),
|
||||||
"id": str(uuid.uuid4()),
|
"id": str(uuid.uuid4()),
|
||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
|
@ -17,6 +17,25 @@ from sqlalchemy.sql import exists
|
|||||||
####################
|
####################
|
||||||
|
|
||||||
|
|
||||||
|
class MessageReaction(Base):
|
||||||
|
__tablename__ = "message_reaction"
|
||||||
|
id = Column(Text, primary_key=True)
|
||||||
|
user_id = Column(Text)
|
||||||
|
message_id = Column(Text)
|
||||||
|
name = Column(Text)
|
||||||
|
created_at = Column(BigInteger)
|
||||||
|
|
||||||
|
|
||||||
|
class MessageReactionModel(BaseModel):
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
id: str
|
||||||
|
user_id: str
|
||||||
|
message_id: str
|
||||||
|
name: str
|
||||||
|
created_at: int # timestamp in epoch
|
||||||
|
|
||||||
|
|
||||||
class Message(Base):
|
class Message(Base):
|
||||||
__tablename__ = "message"
|
__tablename__ = "message"
|
||||||
id = Column(Text, primary_key=True)
|
id = Column(Text, primary_key=True)
|
||||||
@ -24,6 +43,8 @@ class Message(Base):
|
|||||||
user_id = Column(Text)
|
user_id = Column(Text)
|
||||||
channel_id = Column(Text, nullable=True)
|
channel_id = Column(Text, nullable=True)
|
||||||
|
|
||||||
|
parent_id = Column(Text, nullable=True)
|
||||||
|
|
||||||
content = Column(Text)
|
content = Column(Text)
|
||||||
data = Column(JSON, nullable=True)
|
data = Column(JSON, nullable=True)
|
||||||
meta = Column(JSON, nullable=True)
|
meta = Column(JSON, nullable=True)
|
||||||
@ -39,6 +60,8 @@ class MessageModel(BaseModel):
|
|||||||
user_id: str
|
user_id: str
|
||||||
channel_id: Optional[str] = None
|
channel_id: Optional[str] = None
|
||||||
|
|
||||||
|
parent_id: Optional[str] = None
|
||||||
|
|
||||||
content: str
|
content: str
|
||||||
data: Optional[dict] = None
|
data: Optional[dict] = None
|
||||||
meta: Optional[dict] = None
|
meta: Optional[dict] = None
|
||||||
@ -54,10 +77,21 @@ class MessageModel(BaseModel):
|
|||||||
|
|
||||||
class MessageForm(BaseModel):
|
class MessageForm(BaseModel):
|
||||||
content: str
|
content: str
|
||||||
|
parent_id: Optional[str] = None
|
||||||
data: Optional[dict] = None
|
data: Optional[dict] = None
|
||||||
meta: Optional[dict] = None
|
meta: Optional[dict] = None
|
||||||
|
|
||||||
|
|
||||||
|
class Reactions(BaseModel):
|
||||||
|
name: str
|
||||||
|
user_ids: list[str]
|
||||||
|
count: int
|
||||||
|
|
||||||
|
|
||||||
|
class MessageResponse(MessageModel):
|
||||||
|
reactions: list[Reactions]
|
||||||
|
|
||||||
|
|
||||||
class MessageTable:
|
class MessageTable:
|
||||||
def insert_new_message(
|
def insert_new_message(
|
||||||
self, form_data: MessageForm, channel_id: str, user_id: str
|
self, form_data: MessageForm, channel_id: str, user_id: str
|
||||||
@ -71,6 +105,7 @@ class MessageTable:
|
|||||||
"id": id,
|
"id": id,
|
||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
"channel_id": channel_id,
|
"channel_id": channel_id,
|
||||||
|
"parent_id": form_data.parent_id,
|
||||||
"content": form_data.content,
|
"content": form_data.content,
|
||||||
"data": form_data.data,
|
"data": form_data.data,
|
||||||
"meta": form_data.meta,
|
"meta": form_data.meta,
|
||||||
@ -85,10 +120,19 @@ class MessageTable:
|
|||||||
db.refresh(result)
|
db.refresh(result)
|
||||||
return MessageModel.model_validate(result) if result else None
|
return MessageModel.model_validate(result) if result else None
|
||||||
|
|
||||||
def get_message_by_id(self, id: str) -> Optional[MessageModel]:
|
def get_message_by_id(self, id: str) -> Optional[MessageResponse]:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
message = db.get(Message, id)
|
message = db.get(Message, id)
|
||||||
return MessageModel.model_validate(message) if message else None
|
if not message:
|
||||||
|
return None
|
||||||
|
|
||||||
|
reactions = self.get_reactions_by_message_id(id)
|
||||||
|
return MessageResponse(
|
||||||
|
**{
|
||||||
|
**MessageModel.model_validate(message).model_dump(),
|
||||||
|
"reactions": reactions,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def get_messages_by_channel_id(
|
def get_messages_by_channel_id(
|
||||||
self, channel_id: str, skip: int = 0, limit: int = 50
|
self, channel_id: str, skip: int = 0, limit: int = 50
|
||||||
@ -104,20 +148,6 @@ class MessageTable:
|
|||||||
)
|
)
|
||||||
return [MessageModel.model_validate(message) for message in all_messages]
|
return [MessageModel.model_validate(message) for message in all_messages]
|
||||||
|
|
||||||
def get_messages_by_user_id(
|
|
||||||
self, user_id: str, skip: int = 0, limit: int = 50
|
|
||||||
) -> list[MessageModel]:
|
|
||||||
with get_db() as db:
|
|
||||||
all_messages = (
|
|
||||||
db.query(Message)
|
|
||||||
.filter_by(user_id=user_id)
|
|
||||||
.order_by(Message.created_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]:
|
||||||
@ -131,9 +161,58 @@ 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 add_reaction_to_message(
|
||||||
|
self, id: str, user_id: str, name: str
|
||||||
|
) -> Optional[MessageReactionModel]:
|
||||||
|
with get_db() as db:
|
||||||
|
reaction_id = str(uuid.uuid4())
|
||||||
|
reaction = MessageReactionModel(
|
||||||
|
id=reaction_id,
|
||||||
|
user_id=user_id,
|
||||||
|
message_id=id,
|
||||||
|
name=name,
|
||||||
|
created_at=int(time.time_ns()),
|
||||||
|
)
|
||||||
|
result = MessageReaction(**reaction.model_dump())
|
||||||
|
db.add(result)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(result)
|
||||||
|
return MessageReactionModel.model_validate(result) if result else None
|
||||||
|
|
||||||
|
def get_reactions_by_message_id(self, id: str) -> list[Reactions]:
|
||||||
|
with get_db() as db:
|
||||||
|
all_reactions = db.query(MessageReaction).filter_by(message_id=id).all()
|
||||||
|
|
||||||
|
reactions = {}
|
||||||
|
for reaction in all_reactions:
|
||||||
|
if reaction.name not in reactions:
|
||||||
|
reactions[reaction.name] = {
|
||||||
|
"name": reaction.name,
|
||||||
|
"user_ids": [],
|
||||||
|
"count": 0,
|
||||||
|
}
|
||||||
|
reactions[reaction.name]["user_ids"].append(reaction.user_id)
|
||||||
|
reactions[reaction.name]["count"] += 1
|
||||||
|
|
||||||
|
return [Reactions(**reaction) for reaction in reactions.values()]
|
||||||
|
|
||||||
|
def remove_reaction_by_id_and_user_id_and_name(
|
||||||
|
self, id: str, user_id: str, name: str
|
||||||
|
) -> bool:
|
||||||
|
with get_db() as db:
|
||||||
|
db.query(MessageReaction).filter_by(
|
||||||
|
message_id=id, user_id=user_id, name=name
|
||||||
|
).delete()
|
||||||
|
db.commit()
|
||||||
|
return True
|
||||||
|
|
||||||
def delete_message_by_id(self, id: str) -> bool:
|
def delete_message_by_id(self, id: str) -> bool:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
db.query(Message).filter_by(id=id).delete()
|
db.query(Message).filter_by(id=id).delete()
|
||||||
|
|
||||||
|
# Delete all reactions to this message
|
||||||
|
db.query(MessageReaction).filter_by(message_id=id).delete()
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -11,7 +11,12 @@ from open_webui.socket.main import sio, get_user_ids_from_room
|
|||||||
from open_webui.models.users import Users, UserNameResponse
|
from open_webui.models.users import Users, UserNameResponse
|
||||||
|
|
||||||
from open_webui.models.channels import Channels, ChannelModel, ChannelForm
|
from open_webui.models.channels import Channels, ChannelModel, ChannelForm
|
||||||
from open_webui.models.messages import Messages, MessageModel, MessageForm
|
from open_webui.models.messages import (
|
||||||
|
Messages,
|
||||||
|
MessageModel,
|
||||||
|
MessageResponse,
|
||||||
|
MessageForm,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
from open_webui.config import ENABLE_ADMIN_CHAT_ACCESS, ENABLE_ADMIN_EXPORT
|
from open_webui.config import ENABLE_ADMIN_CHAT_ACCESS, ENABLE_ADMIN_EXPORT
|
||||||
@ -49,7 +54,7 @@ async def get_channels(user=Depends(get_verified_user)):
|
|||||||
@router.post("/create", response_model=Optional[ChannelModel])
|
@router.post("/create", response_model=Optional[ChannelModel])
|
||||||
async def create_new_channel(form_data: ChannelForm, user=Depends(get_admin_user)):
|
async def create_new_channel(form_data: ChannelForm, user=Depends(get_admin_user)):
|
||||||
try:
|
try:
|
||||||
channel = Channels.insert_new_channel(form_data, user.id)
|
channel = Channels.insert_new_channel(None, form_data, user.id)
|
||||||
return ChannelModel(**channel.model_dump())
|
return ChannelModel(**channel.model_dump())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
@ -134,11 +139,11 @@ async def delete_channel_by_id(id: str, user=Depends(get_admin_user)):
|
|||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
class MessageUserModel(MessageModel):
|
class MessageUserResponse(MessageResponse):
|
||||||
user: UserNameResponse
|
user: UserNameResponse
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{id}/messages", response_model=list[MessageUserModel])
|
@router.get("/{id}/messages", response_model=list[MessageUserResponse])
|
||||||
async def get_channel_messages(
|
async def get_channel_messages(
|
||||||
id: str, skip: int = 0, limit: int = 50, user=Depends(get_verified_user)
|
id: str, skip: int = 0, limit: int = 50, user=Depends(get_verified_user)
|
||||||
):
|
):
|
||||||
@ -165,9 +170,10 @@ async def get_channel_messages(
|
|||||||
users[message.user_id] = user
|
users[message.user_id] = user
|
||||||
|
|
||||||
messages.append(
|
messages.append(
|
||||||
MessageUserModel(
|
MessageUserResponse(
|
||||||
**{
|
**{
|
||||||
**message.model_dump(),
|
**message.model_dump(),
|
||||||
|
"reactions": Messages.get_reactions_by_message_id(message.id),
|
||||||
"user": UserNameResponse(**users[message.user_id].model_dump()),
|
"user": UserNameResponse(**users[message.user_id].model_dump()),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -333,6 +339,140 @@ async def update_message_by_id(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
############################
|
||||||
|
# AddReactionToMessage
|
||||||
|
############################
|
||||||
|
|
||||||
|
|
||||||
|
class ReactionForm(BaseModel):
|
||||||
|
name: str
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{id}/messages/{message_id}/reactions/add", response_model=bool)
|
||||||
|
async def add_reaction_to_message(
|
||||||
|
id: str, message_id: str, form_data: ReactionForm, 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 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.add_reaction_to_message(message_id, user.id, form_data.name)
|
||||||
|
|
||||||
|
message = Messages.get_message_by_id(message_id)
|
||||||
|
await sio.emit(
|
||||||
|
"channel-events",
|
||||||
|
{
|
||||||
|
"channel_id": channel.id,
|
||||||
|
"message_id": message.id,
|
||||||
|
"data": {
|
||||||
|
"type": "message:reaction",
|
||||||
|
"data": {
|
||||||
|
**message.model_dump(),
|
||||||
|
"user": UserNameResponse(**user.model_dump()).model_dump(),
|
||||||
|
"name": form_data.name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"user": UserNameResponse(**user.model_dump()).model_dump(),
|
||||||
|
"channel": channel.model_dump(),
|
||||||
|
},
|
||||||
|
to=f"channel:{channel.id}",
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(e)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
############################
|
||||||
|
# RemoveReactionById
|
||||||
|
############################
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{id}/messages/{message_id}/reactions/remove", response_model=bool)
|
||||||
|
async def remove_reaction_by_id_and_user_id_and_name(
|
||||||
|
id: str, message_id: str, form_data: ReactionForm, 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 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.remove_reaction_by_id_and_user_id_and_name(
|
||||||
|
message_id, user.id, form_data.name
|
||||||
|
)
|
||||||
|
|
||||||
|
message = Messages.get_message_by_id(message_id)
|
||||||
|
await sio.emit(
|
||||||
|
"channel-events",
|
||||||
|
{
|
||||||
|
"channel_id": channel.id,
|
||||||
|
"message_id": message.id,
|
||||||
|
"data": {
|
||||||
|
"type": "message:reaction",
|
||||||
|
"data": {
|
||||||
|
**message.model_dump(),
|
||||||
|
"user": UserNameResponse(**user.model_dump()).model_dump(),
|
||||||
|
"name": form_data.name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"user": UserNameResponse(**user.model_dump()).model_dump(),
|
||||||
|
"channel": channel.model_dump(),
|
||||||
|
},
|
||||||
|
to=f"channel:{channel.id}",
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(e)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
# DeleteMessageById
|
# DeleteMessageById
|
||||||
############################
|
############################
|
||||||
|
@ -285,6 +285,77 @@ export const updateMessage = async (
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const addReaction = async (token: string = '', channel_id: string, message_id: string, name: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(
|
||||||
|
`${WEBUI_API_BASE_URL}/channels/${channel_id}/messages/${message_id}/reactions/add`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
authorization: `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ name })
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then((json) => {
|
||||||
|
return json;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = err.detail;
|
||||||
|
console.log(err);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const removeReaction = async (token: string = '', channel_id: string, message_id: string, name: string) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(
|
||||||
|
`${WEBUI_API_BASE_URL}/channels/${channel_id}/messages/${message_id}/reactions/remove`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
authorization: `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ name })
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) throw await res.json();
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then((json) => {
|
||||||
|
return json;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = err.detail;
|
||||||
|
console.log(err);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
export const deleteMessage = async (token: string = '', channel_id: string, message_id: string) => {
|
export const deleteMessage = async (token: string = '', channel_id: string, message_id: string) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { onDestroy, onMount, tick } from 'svelte';
|
import { onDestroy, onMount, tick } from 'svelte';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
import { chatId, showSidebar, socket, user } from '$lib/stores';
|
import { chatId, showSidebar, socket, user } from '$lib/stores';
|
||||||
import { getChannelById, getChannelMessages, sendMessage } from '$lib/apis/channels';
|
import { getChannelById, getChannelMessages, sendMessage } from '$lib/apis/channels';
|
||||||
|
|
||||||
import Messages from './Messages.svelte';
|
import Messages from './Messages.svelte';
|
||||||
import MessageInput from './MessageInput.svelte';
|
import MessageInput from './MessageInput.svelte';
|
||||||
import { goto } from '$app/navigation';
|
|
||||||
import Navbar from './Navbar.svelte';
|
import Navbar from './Navbar.svelte';
|
||||||
|
|
||||||
export let id = '';
|
export let id = '';
|
||||||
@ -84,6 +84,13 @@
|
|||||||
} else if (type === 'message:delete') {
|
} else if (type === 'message:delete') {
|
||||||
console.log('message:delete', data);
|
console.log('message:delete', data);
|
||||||
messages = messages.filter((message) => message.id !== data.id);
|
messages = messages.filter((message) => message.id !== data.id);
|
||||||
|
} else if (type === 'message:reaction') {
|
||||||
|
console.log('message:reaction', data);
|
||||||
|
|
||||||
|
const idx = messages.findIndex((message) => message.id === data.id);
|
||||||
|
if (idx !== -1) {
|
||||||
|
messages[idx] = data;
|
||||||
|
}
|
||||||
} else if (type === 'typing') {
|
} else if (type === 'typing') {
|
||||||
if (event.user.id === $user.id) {
|
if (event.user.id === $user.id) {
|
||||||
return;
|
return;
|
||||||
|
@ -11,12 +11,12 @@
|
|||||||
dayjs.extend(isYesterday);
|
dayjs.extend(isYesterday);
|
||||||
import { tick, getContext, onMount, createEventDispatcher } from 'svelte';
|
import { tick, getContext, onMount, createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
import { settings } from '$lib/stores';
|
import { settings, user } from '$lib/stores';
|
||||||
|
|
||||||
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 { deleteMessage, updateMessage } from '$lib/apis/channels';
|
import { addReaction, deleteMessage, removeReaction, updateMessage } from '$lib/apis/channels';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
@ -109,6 +109,31 @@
|
|||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
onReaction={(name) => {
|
||||||
|
if (
|
||||||
|
message.reactions
|
||||||
|
.find((reaction) => reaction.name === name)
|
||||||
|
?.user_ids?.includes($user.id) ??
|
||||||
|
false
|
||||||
|
) {
|
||||||
|
const res = removeReaction(
|
||||||
|
localStorage.token,
|
||||||
|
message.channel_id,
|
||||||
|
message.id,
|
||||||
|
name
|
||||||
|
).catch((error) => {
|
||||||
|
toast.error(error);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const res = addReaction(localStorage.token, message.channel_id, message.id, name).catch(
|
||||||
|
(error) => {
|
||||||
|
toast.error(error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
|
@ -35,25 +35,14 @@
|
|||||||
|
|
||||||
export let onDelete: Function = () => {};
|
export let onDelete: Function = () => {};
|
||||||
export let onEdit: Function = () => {};
|
export let onEdit: Function = () => {};
|
||||||
|
export let onReaction: Function = () => {};
|
||||||
|
|
||||||
let showButtons = false;
|
let showButtons = false;
|
||||||
|
|
||||||
let edit = false;
|
let edit = false;
|
||||||
let editedContent = null;
|
let editedContent = null;
|
||||||
let showDeleteConfirmDialog = false;
|
let showDeleteConfirmDialog = false;
|
||||||
|
|
||||||
let reactions = [
|
|
||||||
{
|
|
||||||
name: 'red_circle',
|
|
||||||
user_ids: ['U07KUHZSYER'],
|
|
||||||
count: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '+1',
|
|
||||||
user_ids: [$user.id],
|
|
||||||
count: 1
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const formatDate = (inputDate) => {
|
const formatDate = (inputDate) => {
|
||||||
const date = dayjs(inputDate);
|
const date = dayjs(inputDate);
|
||||||
const now = dayjs();
|
const now = dayjs();
|
||||||
@ -92,7 +81,13 @@
|
|||||||
<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-800"
|
class="flex gap-1 rounded-lg bg-white dark:bg-gray-850 shadow-md p-0.5 border border-gray-100 dark:border-gray-800"
|
||||||
>
|
>
|
||||||
<ReactionPicker onClose={() => (showButtons = false)}>
|
<ReactionPicker
|
||||||
|
onClose={() => (showButtons = false)}
|
||||||
|
onSubmit={(name) => {
|
||||||
|
showButtons = false;
|
||||||
|
onReaction(name);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Tooltip content={$i18n.t('Add Reaction')}>
|
<Tooltip content={$i18n.t('Add Reaction')}>
|
||||||
<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"
|
||||||
@ -271,38 +266,49 @@
|
|||||||
>{/if}
|
>{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if reactions.length > 0}
|
{#if message.reactions.length > 0}
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center gap-1 mt-1 mb-2">
|
<div class="flex items-center gap-1 mt-1 mb-2">
|
||||||
{#each reactions as reaction}
|
{#each message.reactions as reaction}
|
||||||
<button
|
<Tooltip content={`:${reaction.name}:`}>
|
||||||
class="flex items-center gap-1.5 transition rounded-xl px-2 py-1 cursor-pointer {reaction.user_ids.includes(
|
<button
|
||||||
$user.id
|
class="flex items-center gap-1.5 transition rounded-xl px-2 py-1 cursor-pointer {reaction.user_ids.includes(
|
||||||
)
|
$user.id
|
||||||
? ' bg-blue-500/10 outline outline-blue-500/50 outline-1'
|
)
|
||||||
: 'bg-gray-500/10 hover:outline hover:outline-gray-700/30 dark:hover:outline-gray-300/30 hover:outline-1'}"
|
? ' bg-blue-500/10 outline outline-blue-500/50 outline-1'
|
||||||
>
|
: 'bg-gray-500/10 hover:outline hover:outline-gray-700/30 dark:hover:outline-gray-300/30 hover:outline-1'}"
|
||||||
{#if $shortCodesToEmojis[reaction.name]}
|
on:click={() => {
|
||||||
<img
|
onReaction(reaction.name);
|
||||||
src="/assets/emojis/{$shortCodesToEmojis[reaction.name].toLowerCase()}.svg"
|
}}
|
||||||
alt={reaction.name}
|
>
|
||||||
class=" size-4"
|
{#if $shortCodesToEmojis[reaction.name]}
|
||||||
/>
|
<img
|
||||||
{:else}
|
src="/assets/emojis/{$shortCodesToEmojis[
|
||||||
<div>
|
reaction.name
|
||||||
{reaction.name}
|
].toLowerCase()}.svg"
|
||||||
</div>
|
alt={reaction.name}
|
||||||
{/if}
|
class=" size-4"
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div>
|
||||||
|
{reaction.name}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if reaction.user_ids.length > 0}
|
{#if reaction.user_ids.length > 0}
|
||||||
<div class="text-xs font-medium text-gray-500 dark:text-gray-400">
|
<div class="text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||||
{reaction.user_ids?.length}
|
{reaction.user_ids?.length}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
</Tooltip>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
<ReactionPicker>
|
<ReactionPicker
|
||||||
|
onSubmit={(name) => {
|
||||||
|
onReaction(name);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Tooltip content={$i18n.t('Add Reaction')}>
|
<Tooltip content={$i18n.t('Add Reaction')}>
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-1.5 bg-gray-500/10 hover:outline hover:outline-gray-700/30 dark:hover:outline-gray-300/30 hover:outline-1 transition rounded-xl px-1 py-1 cursor-pointer text-gray-500 dark:text-gray-400"
|
class="flex items-center gap-1.5 bg-gray-500/10 hover:outline hover:outline-gray-700/30 dark:hover:outline-gray-300/30 hover:outline-1 transition rounded-xl px-1 py-1 cursor-pointer text-gray-500 dark:text-gray-400"
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
|
||||||
export let onClose = () => {};
|
export let onClose = () => {};
|
||||||
|
export let onSubmit = (name) => {};
|
||||||
export let side = 'top';
|
export let side = 'top';
|
||||||
export let align = 'start';
|
export let align = 'start';
|
||||||
|
|
||||||
@ -95,8 +96,15 @@
|
|||||||
.join(', ')}
|
.join(', ')}
|
||||||
placement="top"
|
placement="top"
|
||||||
>
|
>
|
||||||
<div
|
<button
|
||||||
class="p-1.5 rounded-lg cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700 transition"
|
class="p-1.5 rounded-lg cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700 transition"
|
||||||
|
on:click={() => {
|
||||||
|
typeof emojiShortCodes[emoji] === 'string'
|
||||||
|
? onSubmit(emojiShortCodes[emoji])
|
||||||
|
: onSubmit(emojiShortCodes[emoji][0]);
|
||||||
|
|
||||||
|
show = false;
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src="/assets/emojis/{emoji.toLowerCase()}.svg"
|
src="/assets/emojis/{emoji.toLowerCase()}.svg"
|
||||||
@ -104,7 +112,7 @@
|
|||||||
class="size-5"
|
class="size-5"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
</div>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user