mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-12 04:15:25 +00:00
feat/enh: add/remove users from group channel
This commit is contained in:
parent
781aeebd2a
commit
3f1d9ccbf8
11 changed files with 625 additions and 103 deletions
|
|
@ -5,7 +5,6 @@ from typing import Optional
|
||||||
|
|
||||||
from open_webui.internal.db import Base, get_db
|
from open_webui.internal.db import Base, get_db
|
||||||
from open_webui.models.groups import Groups
|
from open_webui.models.groups import Groups
|
||||||
from open_webui.utils.access_control import has_access
|
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict
|
from pydantic import BaseModel, ConfigDict
|
||||||
from sqlalchemy import BigInteger, Boolean, Column, String, Text, JSON, case
|
from sqlalchemy import BigInteger, Boolean, Column, String, Text, JSON, case
|
||||||
|
|
@ -175,7 +174,9 @@ class ChannelWebhookModel(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class ChannelResponse(ChannelModel):
|
class ChannelResponse(ChannelModel):
|
||||||
|
is_manager: bool = False
|
||||||
write_access: bool = False
|
write_access: bool = False
|
||||||
|
|
||||||
user_count: Optional[int] = None
|
user_count: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -196,32 +197,42 @@ class CreateChannelForm(ChannelForm):
|
||||||
|
|
||||||
class ChannelTable:
|
class ChannelTable:
|
||||||
|
|
||||||
def _create_memberships_by_user_ids_and_group_ids(
|
def _collect_unique_user_ids(
|
||||||
self,
|
self,
|
||||||
channel_id: str,
|
|
||||||
invited_by: str,
|
invited_by: str,
|
||||||
user_ids: Optional[list[str]] = None,
|
user_ids: Optional[list[str]] = None,
|
||||||
group_ids: Optional[list[str]] = None,
|
group_ids: Optional[list[str]] = None,
|
||||||
) -> list[ChannelMemberModel]:
|
) -> set[str]:
|
||||||
# For group and direct message channels, automatically add the specified users as members
|
"""
|
||||||
user_ids = user_ids or []
|
Collect unique user ids from:
|
||||||
if invited_by not in user_ids:
|
- invited_by
|
||||||
user_ids.append(invited_by) # Ensure the creator is also a member
|
- user_ids
|
||||||
|
- each group in group_ids
|
||||||
|
Returns a set for efficient SQL diffing.
|
||||||
|
"""
|
||||||
|
users = set(user_ids or [])
|
||||||
|
users.add(invited_by)
|
||||||
|
|
||||||
# Add users from specified groups
|
for group_id in group_ids or []:
|
||||||
group_ids = group_ids or []
|
users.update(Groups.get_group_user_ids_by_id(group_id))
|
||||||
for group_id in group_ids:
|
|
||||||
group_user_ids = Groups.get_group_user_ids_by_id(group_id)
|
|
||||||
for uid in group_user_ids:
|
|
||||||
if uid not in user_ids:
|
|
||||||
user_ids.append(uid)
|
|
||||||
|
|
||||||
# Ensure uniqueness
|
return users
|
||||||
user_ids = list(set(user_ids))
|
|
||||||
|
|
||||||
|
def _create_membership_models(
|
||||||
|
self,
|
||||||
|
channel_id: str,
|
||||||
|
invited_by: str,
|
||||||
|
user_ids: set[str],
|
||||||
|
) -> list[ChannelMember]:
|
||||||
|
"""
|
||||||
|
Takes a set of NEW user IDs (already filtered to exclude existing members).
|
||||||
|
Returns ORM ChannelMember objects to be added.
|
||||||
|
"""
|
||||||
|
now = int(time.time_ns())
|
||||||
memberships = []
|
memberships = []
|
||||||
|
|
||||||
for uid in user_ids:
|
for uid in user_ids:
|
||||||
channel_member = ChannelMemberModel(
|
model = ChannelMemberModel(
|
||||||
**{
|
**{
|
||||||
"id": str(uuid.uuid4()),
|
"id": str(uuid.uuid4()),
|
||||||
"channel_id": channel_id,
|
"channel_id": channel_id,
|
||||||
|
|
@ -230,17 +241,16 @@ class ChannelTable:
|
||||||
"is_active": True,
|
"is_active": True,
|
||||||
"is_channel_muted": False,
|
"is_channel_muted": False,
|
||||||
"is_channel_pinned": False,
|
"is_channel_pinned": False,
|
||||||
"invited_at": int(time.time_ns()),
|
"invited_at": now,
|
||||||
"invited_by": invited_by,
|
"invited_by": invited_by,
|
||||||
"joined_at": int(time.time_ns()),
|
"joined_at": now,
|
||||||
"left_at": None,
|
"left_at": None,
|
||||||
"last_read_at": int(time.time_ns()),
|
"last_read_at": now,
|
||||||
"created_at": int(time.time_ns()),
|
"created_at": now,
|
||||||
"updated_at": int(time.time_ns()),
|
"updated_at": now,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
memberships.append(ChannelMember(**model.model_dump()))
|
||||||
memberships.append(ChannelMember(**channel_member.model_dump()))
|
|
||||||
|
|
||||||
return memberships
|
return memberships
|
||||||
|
|
||||||
|
|
@ -262,14 +272,18 @@ class ChannelTable:
|
||||||
new_channel = Channel(**channel.model_dump())
|
new_channel = Channel(**channel.model_dump())
|
||||||
|
|
||||||
if form_data.type in ["group", "dm"]:
|
if form_data.type in ["group", "dm"]:
|
||||||
memberships = self._create_memberships_by_user_ids_and_group_ids(
|
users = self._collect_unique_user_ids(
|
||||||
channel.id,
|
invited_by=user_id,
|
||||||
user_id,
|
user_ids=form_data.user_ids,
|
||||||
form_data.user_ids,
|
group_ids=form_data.group_ids,
|
||||||
form_data.group_ids,
|
)
|
||||||
|
memberships = self._create_membership_models(
|
||||||
|
channel_id=new_channel.id,
|
||||||
|
invited_by=user_id,
|
||||||
|
user_ids=users,
|
||||||
)
|
)
|
||||||
db.add_all(memberships)
|
|
||||||
|
|
||||||
|
db.add_all(memberships)
|
||||||
db.add(new_channel)
|
db.add(new_channel)
|
||||||
db.commit()
|
db.commit()
|
||||||
return channel
|
return channel
|
||||||
|
|
@ -279,24 +293,71 @@ class ChannelTable:
|
||||||
channels = db.query(Channel).all()
|
channels = db.query(Channel).all()
|
||||||
return [ChannelModel.model_validate(channel) for channel in channels]
|
return [ChannelModel.model_validate(channel) for channel in channels]
|
||||||
|
|
||||||
def get_channels_by_user_id(
|
def _has_permission(self, query, filter: dict, permission: str = "read"):
|
||||||
self, user_id: str, permission: str = "read"
|
group_ids = filter.get("group_ids", [])
|
||||||
) -> list[ChannelModel]:
|
user_id = filter.get("user_id")
|
||||||
channels = self.get_channels()
|
|
||||||
|
|
||||||
channel_list = []
|
json_group_ids = Channel.access_control[permission]["group_ids"]
|
||||||
for channel in channels:
|
|
||||||
if channel.type == "dm":
|
|
||||||
membership = self.get_member_by_channel_and_user_id(channel.id, user_id)
|
|
||||||
if membership and membership.is_active:
|
|
||||||
channel_list.append(channel)
|
|
||||||
else:
|
|
||||||
if channel.user_id == user_id or has_access(
|
|
||||||
user_id, permission, channel.access_control
|
|
||||||
):
|
|
||||||
channel_list.append(channel)
|
|
||||||
|
|
||||||
return channel_list
|
conditions = []
|
||||||
|
if group_ids or user_id:
|
||||||
|
conditions.append(Channel.access_control.is_(None))
|
||||||
|
|
||||||
|
if user_id:
|
||||||
|
conditions.append(Channel.user_id == user_id)
|
||||||
|
|
||||||
|
if group_ids:
|
||||||
|
group_conditions = []
|
||||||
|
|
||||||
|
for gid in group_ids:
|
||||||
|
# CASE: gid IN JSON array
|
||||||
|
# SQLite → json_extract(access_control, '$.write.group_ids') LIKE '%gid%'
|
||||||
|
# Postgres → access_control->'write'->'group_ids' @> '[gid]'
|
||||||
|
group_conditions.append(json_group_ids.contains([gid]))
|
||||||
|
|
||||||
|
conditions.append(or_(*group_conditions))
|
||||||
|
|
||||||
|
if conditions:
|
||||||
|
query = query.filter(or_(*conditions))
|
||||||
|
|
||||||
|
return query
|
||||||
|
|
||||||
|
def get_channels_by_user_id(self, user_id: str) -> list[ChannelModel]:
|
||||||
|
with get_db() as db:
|
||||||
|
user_group_ids = [
|
||||||
|
group.id for group in Groups.get_groups_by_member_id(user_id)
|
||||||
|
]
|
||||||
|
|
||||||
|
membership_channels = (
|
||||||
|
db.query(Channel)
|
||||||
|
.join(ChannelMember, Channel.id == ChannelMember.channel_id)
|
||||||
|
.filter(
|
||||||
|
Channel.deleted_at.is_(None),
|
||||||
|
Channel.archived_at.is_(None),
|
||||||
|
Channel.type.in_(["group", "dm"]),
|
||||||
|
ChannelMember.user_id == user_id,
|
||||||
|
ChannelMember.is_active.is_(True),
|
||||||
|
)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
query = db.query(Channel).filter(
|
||||||
|
Channel.deleted_at.is_(None),
|
||||||
|
Channel.archived_at.is_(None),
|
||||||
|
or_(
|
||||||
|
Channel.type.is_(None), # True NULL/None
|
||||||
|
Channel.type == "", # Empty string
|
||||||
|
and_(Channel.type != "group", Channel.type != "dm"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
query = self._has_permission(
|
||||||
|
query, {"user_id": user_id, "group_ids": user_group_ids}
|
||||||
|
)
|
||||||
|
|
||||||
|
standard_channels = query.all()
|
||||||
|
|
||||||
|
all_channels = membership_channels + standard_channels
|
||||||
|
return [ChannelModel.model_validate(c) for c in all_channels]
|
||||||
|
|
||||||
def get_dm_channel_by_user_ids(self, user_ids: list[str]) -> Optional[ChannelModel]:
|
def get_dm_channel_by_user_ids(self, user_ids: list[str]) -> Optional[ChannelModel]:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
|
|
@ -331,6 +392,78 @@ class ChannelTable:
|
||||||
|
|
||||||
return ChannelModel.model_validate(channel) if channel else None
|
return ChannelModel.model_validate(channel) if channel else None
|
||||||
|
|
||||||
|
def add_members_to_channel(
|
||||||
|
self,
|
||||||
|
channel_id: str,
|
||||||
|
invited_by: str,
|
||||||
|
user_ids: Optional[list[str]] = None,
|
||||||
|
group_ids: Optional[list[str]] = None,
|
||||||
|
) -> list[ChannelMemberModel]:
|
||||||
|
with get_db() as db:
|
||||||
|
# 1. Collect all user_ids including groups + inviter
|
||||||
|
requested_users = self._collect_unique_user_ids(
|
||||||
|
invited_by, user_ids, group_ids
|
||||||
|
)
|
||||||
|
|
||||||
|
existing_users = {
|
||||||
|
row.user_id
|
||||||
|
for row in db.query(ChannelMember.user_id)
|
||||||
|
.filter(ChannelMember.channel_id == channel_id)
|
||||||
|
.all()
|
||||||
|
}
|
||||||
|
|
||||||
|
new_user_ids = requested_users - existing_users
|
||||||
|
if not new_user_ids:
|
||||||
|
return [] # Nothing to add
|
||||||
|
|
||||||
|
new_memberships = self._create_membership_models(
|
||||||
|
channel_id, invited_by, new_user_ids
|
||||||
|
)
|
||||||
|
|
||||||
|
db.add_all(new_memberships)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return [
|
||||||
|
ChannelMemberModel.model_validate(membership)
|
||||||
|
for membership in new_memberships
|
||||||
|
]
|
||||||
|
|
||||||
|
def remove_members_from_channel(
|
||||||
|
self,
|
||||||
|
channel_id: str,
|
||||||
|
user_ids: list[str],
|
||||||
|
) -> int:
|
||||||
|
with get_db() as db:
|
||||||
|
result = (
|
||||||
|
db.query(ChannelMember)
|
||||||
|
.filter(
|
||||||
|
ChannelMember.channel_id == channel_id,
|
||||||
|
ChannelMember.user_id.in_(user_ids),
|
||||||
|
)
|
||||||
|
.delete(synchronize_session=False)
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
return result # number of rows deleted
|
||||||
|
|
||||||
|
def is_user_channel_manager(self, channel_id: str, user_id: str) -> bool:
|
||||||
|
with get_db() as db:
|
||||||
|
# Check if the user is the creator of the channel
|
||||||
|
# or has a 'manager' role in ChannelMember
|
||||||
|
channel = db.query(Channel).filter(Channel.id == channel_id).first()
|
||||||
|
if channel and channel.user_id == user_id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
membership = (
|
||||||
|
db.query(ChannelMember)
|
||||||
|
.filter(
|
||||||
|
ChannelMember.channel_id == channel_id,
|
||||||
|
ChannelMember.user_id == user_id,
|
||||||
|
ChannelMember.role == "manager",
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
return membership is not None
|
||||||
|
|
||||||
def join_channel(
|
def join_channel(
|
||||||
self, channel_id: str, user_id: str
|
self, channel_id: str, user_id: str
|
||||||
) -> Optional[ChannelMemberModel]:
|
) -> Optional[ChannelMemberModel]:
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,9 @@ from open_webui.internal.db import Base, JSONField, get_db
|
||||||
from open_webui.env import DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL
|
from open_webui.env import DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL
|
||||||
from open_webui.models.chats import Chats
|
from open_webui.models.chats import Chats
|
||||||
from open_webui.models.groups import Groups, GroupMember
|
from open_webui.models.groups import Groups, GroupMember
|
||||||
|
from open_webui.models.channels import ChannelMember
|
||||||
|
|
||||||
|
|
||||||
from open_webui.utils.misc import throttle
|
from open_webui.utils.misc import throttle
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -311,6 +314,17 @@ class UsersTable:
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
channel_id = filter.get("channel_id")
|
||||||
|
if channel_id:
|
||||||
|
query = query.filter(
|
||||||
|
exists(
|
||||||
|
select(ChannelMember.id).where(
|
||||||
|
ChannelMember.user_id == User.id,
|
||||||
|
ChannelMember.channel_id == channel_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
user_ids = filter.get("user_ids")
|
user_ids = filter.get("user_ids")
|
||||||
group_ids = filter.get("group_ids")
|
group_ids = filter.get("group_ids")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -261,6 +261,7 @@ async def get_channel_by_id(id: str, user=Depends(get_verified_user)):
|
||||||
**channel.model_dump(),
|
**channel.model_dump(),
|
||||||
"user_ids": user_ids,
|
"user_ids": user_ids,
|
||||||
"users": users,
|
"users": users,
|
||||||
|
"is_manager": Channels.is_user_channel_manager(channel.id, user.id),
|
||||||
"write_access": True,
|
"write_access": True,
|
||||||
"user_count": len(user_ids),
|
"user_count": len(user_ids),
|
||||||
"last_read_at": channel_member.last_read_at if channel_member else None,
|
"last_read_at": channel_member.last_read_at if channel_member else None,
|
||||||
|
|
@ -291,6 +292,7 @@ async def get_channel_by_id(id: str, user=Depends(get_verified_user)):
|
||||||
**channel.model_dump(),
|
**channel.model_dump(),
|
||||||
"user_ids": user_ids,
|
"user_ids": user_ids,
|
||||||
"users": users,
|
"users": users,
|
||||||
|
"is_manager": Channels.is_user_channel_manager(channel.id, user.id),
|
||||||
"write_access": write_access or user.role == "admin",
|
"write_access": write_access or user.role == "admin",
|
||||||
"user_count": user_count,
|
"user_count": user_count,
|
||||||
"last_read_at": channel_member.last_read_at if channel_member else None,
|
"last_read_at": channel_member.last_read_at if channel_member else None,
|
||||||
|
|
@ -334,6 +336,7 @@ async def get_channel_members_by_id(
|
||||||
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if channel.type == "dm":
|
||||||
user_ids = [
|
user_ids = [
|
||||||
member.user_id for member in Channels.get_members_by_channel_id(channel.id)
|
member.user_id for member in Channels.get_members_by_channel_id(channel.id)
|
||||||
]
|
]
|
||||||
|
|
@ -349,11 +352,8 @@ async def get_channel_members_by_id(
|
||||||
],
|
],
|
||||||
"total": total,
|
"total": total,
|
||||||
}
|
}
|
||||||
|
|
||||||
else:
|
else:
|
||||||
filter = {
|
filter = {}
|
||||||
"roles": ["!pending"],
|
|
||||||
}
|
|
||||||
|
|
||||||
if query:
|
if query:
|
||||||
filter["query"] = query
|
filter["query"] = query
|
||||||
|
|
@ -362,7 +362,13 @@ async def get_channel_members_by_id(
|
||||||
if direction:
|
if direction:
|
||||||
filter["direction"] = direction
|
filter["direction"] = direction
|
||||||
|
|
||||||
permitted_ids = get_permitted_group_and_user_ids("read", channel.access_control)
|
if channel.type == "group":
|
||||||
|
filter["channel_id"] = channel.id
|
||||||
|
else:
|
||||||
|
filter["roles"] = ["!pending"]
|
||||||
|
permitted_ids = get_permitted_group_and_user_ids(
|
||||||
|
"read", channel.access_control
|
||||||
|
)
|
||||||
if permitted_ids:
|
if permitted_ids:
|
||||||
filter["user_ids"] = permitted_ids.get("user_ids")
|
filter["user_ids"] = permitted_ids.get("user_ids")
|
||||||
filter["group_ids"] = permitted_ids.get("group_ids")
|
filter["group_ids"] = permitted_ids.get("group_ids")
|
||||||
|
|
@ -413,6 +419,101 @@ async def update_is_active_member_by_id_and_user_id(
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
#################################################
|
||||||
|
# AddMembersById
|
||||||
|
#################################################
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateMembersForm(BaseModel):
|
||||||
|
user_ids: list[str] = []
|
||||||
|
group_ids: list[str] = []
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{id}/update/members/add")
|
||||||
|
async def add_members_by_id(
|
||||||
|
request: Request,
|
||||||
|
id: str,
|
||||||
|
form_data: UpdateMembersForm,
|
||||||
|
user=Depends(get_verified_user),
|
||||||
|
):
|
||||||
|
if user.role != "admin" and not has_permission(
|
||||||
|
user.id, "features.channels", request.app.state.config.USER_PERMISSIONS
|
||||||
|
):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
||||||
|
)
|
||||||
|
|
||||||
|
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.user_id != user.id and user.role != "admin":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
memberships = Channels.add_members_to_channel(
|
||||||
|
channel.id, user.id, form_data.user_ids, form_data.group_ids
|
||||||
|
)
|
||||||
|
|
||||||
|
return memberships
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(e)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#################################################
|
||||||
|
#
|
||||||
|
#################################################
|
||||||
|
|
||||||
|
|
||||||
|
class RemoveMembersForm(BaseModel):
|
||||||
|
user_ids: list[str] = []
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{id}/update/members/remove")
|
||||||
|
async def remove_members_by_id(
|
||||||
|
request: Request,
|
||||||
|
id: str,
|
||||||
|
form_data: RemoveMembersForm,
|
||||||
|
user=Depends(get_verified_user),
|
||||||
|
):
|
||||||
|
if user.role != "admin" and not has_permission(
|
||||||
|
user.id, "features.channels", request.app.state.config.USER_PERMISSIONS
|
||||||
|
):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
||||||
|
)
|
||||||
|
|
||||||
|
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.user_id != user.id and user.role != "admin":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
deleted = Channels.remove_members_from_channel(channel.id, form_data.user_ids)
|
||||||
|
|
||||||
|
return deleted
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(e)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
# UpdateChannelById
|
# UpdateChannelById
|
||||||
############################
|
############################
|
||||||
|
|
|
||||||
|
|
@ -309,11 +309,13 @@ async def user_join(sid, data):
|
||||||
)
|
)
|
||||||
|
|
||||||
await sio.enter_room(sid, f"user:{user.id}")
|
await sio.enter_room(sid, f"user:{user.id}")
|
||||||
|
|
||||||
# Join all the channels
|
# Join all the channels
|
||||||
channels = Channels.get_channels_by_user_id(user.id)
|
channels = Channels.get_channels_by_user_id(user.id)
|
||||||
log.debug(f"{channels=}")
|
log.debug(f"{channels=}")
|
||||||
for channel in channels:
|
for channel in channels:
|
||||||
await sio.enter_room(sid, f"channel:{channel.id}")
|
await sio.enter_room(sid, f"channel:{channel.id}")
|
||||||
|
|
||||||
return {"id": user.id, "name": user.name}
|
return {"id": user.id, "name": user.name}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -194,6 +194,88 @@ export const updateChannelMemberActiveStatusById = async (
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type UpdateMembersForm = {
|
||||||
|
user_ids?: string[];
|
||||||
|
group_ids?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addMembersById = async (
|
||||||
|
token: string = '',
|
||||||
|
channel_id: string,
|
||||||
|
formData: UpdateMembersForm
|
||||||
|
) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/channels/${channel_id}/update/members/add`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
authorization: `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ ...formData })
|
||||||
|
})
|
||||||
|
.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;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RemoveMembersForm = {
|
||||||
|
user_ids?: string[];
|
||||||
|
group_ids?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeMembersById = async (
|
||||||
|
token: string = '',
|
||||||
|
channel_id: string,
|
||||||
|
formData: RemoveMembersForm
|
||||||
|
) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
const res = await fetch(`${WEBUI_API_BASE_URL}/channels/${channel_id}/update/members/remove`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
authorization: `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ ...formData })
|
||||||
|
})
|
||||||
|
.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 updateChannelById = async (
|
export const updateChannelById = async (
|
||||||
token: string = '',
|
token: string = '',
|
||||||
channel_id: string,
|
channel_id: string,
|
||||||
|
|
|
||||||
|
|
@ -309,6 +309,11 @@
|
||||||
return message;
|
return message;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
onUpdate={async () => {
|
||||||
|
channel = await getChannelById(localStorage.token, id).catch((error) => {
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#if channel && messages !== null}
|
{#if channel && messages !== null}
|
||||||
|
|
|
||||||
|
|
@ -3,22 +3,41 @@
|
||||||
import { getContext, onMount } from 'svelte';
|
import { getContext, onMount } from 'svelte';
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
import { removeMembersById } from '$lib/apis/channels';
|
||||||
|
|
||||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
import Modal from '$lib/components/common/Modal.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 XMark from '$lib/components/icons/XMark.svelte';
|
||||||
import Hashtag from '../icons/Hashtag.svelte';
|
import Hashtag from '../icons/Hashtag.svelte';
|
||||||
import Lock from '../icons/Lock.svelte';
|
import Lock from '../icons/Lock.svelte';
|
||||||
import UserList from './ChannelInfoModal/UserList.svelte';
|
import UserList from './ChannelInfoModal/UserList.svelte';
|
||||||
|
import AddMembersModal from './ChannelInfoModal/AddMembersModal.svelte';
|
||||||
|
|
||||||
export let show = false;
|
export let show = false;
|
||||||
export let channel = null;
|
export let channel = null;
|
||||||
|
|
||||||
|
export let onUpdate = () => {};
|
||||||
|
|
||||||
|
let showAddMembersModal = false;
|
||||||
const submitHandler = async () => {};
|
const submitHandler = async () => {};
|
||||||
|
|
||||||
|
const removeMemberHandler = async (userId) => {
|
||||||
|
const res = await removeMembersById(localStorage.token, channel.id, {
|
||||||
|
user_ids: [userId]
|
||||||
|
}).catch((error) => {
|
||||||
|
toast.error(`${error}`);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
toast.success($i18n.t('Member removed successfully'));
|
||||||
|
onUpdate();
|
||||||
|
} else {
|
||||||
|
toast.error($i18n.t('Failed to remove member'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const init = () => {};
|
const init = () => {};
|
||||||
|
|
||||||
$: if (show) {
|
$: if (show) {
|
||||||
|
|
@ -31,15 +50,14 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if channel}
|
{#if channel}
|
||||||
|
<AddMembersModal bind:show={showAddMembersModal} {channel} {onUpdate} />
|
||||||
<Modal size="sm" bind:show>
|
<Modal size="sm" bind:show>
|
||||||
<div>
|
<div>
|
||||||
<div class=" flex justify-between dark:text-gray-100 px-5 pt-4 mb-1.5">
|
<div class=" flex justify-between dark:text-gray-100 px-5 pt-4 mb-1.5">
|
||||||
<div class="self-center text-base">
|
<div class="self-center text-base">
|
||||||
<div class="flex items-center gap-0.5 shrink-0">
|
<div class="flex items-center gap-0.5 shrink-0">
|
||||||
{#if channel?.type === 'dm'}
|
{#if channel?.type === 'dm'}
|
||||||
<div
|
<div class=" text-left self-center overflow-hidden w-full line-clamp-1 flex-1">
|
||||||
class=" text-left self-center overflow-hidden w-full line-clamp-1 capitalize flex-1"
|
|
||||||
>
|
|
||||||
{$i18n.t('Direct Message')}
|
{$i18n.t('Direct Message')}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
|
@ -51,9 +69,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div class=" text-left self-center overflow-hidden w-full line-clamp-1 flex-1">
|
||||||
class=" text-left self-center overflow-hidden w-full line-clamp-1 capitalize flex-1"
|
|
||||||
>
|
|
||||||
{channel.name}
|
{channel.name}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -69,7 +85,7 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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 md:flex-row w-full px-3 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 sm:flex-row sm:justify-center sm:space-x-6">
|
||||||
<form
|
<form
|
||||||
class="flex flex-col w-full"
|
class="flex flex-col w-full"
|
||||||
|
|
@ -79,7 +95,21 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="flex flex-col w-full h-full pb-2">
|
<div class="flex flex-col w-full h-full pb-2">
|
||||||
<UserList {channel} search={channel?.type !== 'dm'} sort={channel?.type !== 'dm'} />
|
<UserList
|
||||||
|
{channel}
|
||||||
|
onAdd={channel?.type === 'group' && channel?.is_manager
|
||||||
|
? () => {
|
||||||
|
showAddMembersModal = true;
|
||||||
|
}
|
||||||
|
: null}
|
||||||
|
onRemove={channel?.type === 'group' && channel?.is_manager
|
||||||
|
? (userId) => {
|
||||||
|
removeMemberHandler(userId);
|
||||||
|
}
|
||||||
|
: null}
|
||||||
|
search={channel?.type !== 'dm'}
|
||||||
|
sort={channel?.type !== 'dm'}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
import { getContext, onMount } from 'svelte';
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
import { addMembersById } from '$lib/apis/channels';
|
||||||
|
|
||||||
|
import Modal from '$lib/components/common/Modal.svelte';
|
||||||
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
|
import MemberSelector from '$lib/components/workspace/common/MemberSelector.svelte';
|
||||||
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
|
|
||||||
|
export let show = false;
|
||||||
|
export let channel = null;
|
||||||
|
|
||||||
|
export let onUpdate = () => {};
|
||||||
|
|
||||||
|
let groupIds = [];
|
||||||
|
let userIds = [];
|
||||||
|
|
||||||
|
let loading = false;
|
||||||
|
|
||||||
|
const submitHandler = async () => {
|
||||||
|
const res = await addMembersById(localStorage.token, channel.id, {
|
||||||
|
user_ids: userIds,
|
||||||
|
group_ids: groupIds
|
||||||
|
}).catch((error) => {
|
||||||
|
toast.error(`${error}`);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
toast.success($i18n.t('Members added successfully'));
|
||||||
|
onUpdate();
|
||||||
|
show = false;
|
||||||
|
} else {
|
||||||
|
toast.error($i18n.t('Failed to add members'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</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('Add Members')}
|
||||||
|
</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-3 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">
|
||||||
|
<form
|
||||||
|
class="flex flex-col w-full"
|
||||||
|
on:submit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
submitHandler();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="flex flex-col w-full h-full pb-2">
|
||||||
|
<MemberSelector bind:userIds bind:groupIds includeGroups={true} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end pt-3 text-sm font-medium gap-1.5">
|
||||||
|
<button
|
||||||
|
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-950 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full flex flex-row space-x-1 items-center {loading
|
||||||
|
? ' cursor-not-allowed'
|
||||||
|
: ''}"
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{$i18n.t('Add')}
|
||||||
|
|
||||||
|
{#if loading}
|
||||||
|
<div class="ml-2 self-center">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
{/if}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||||
import { WEBUI_NAME, config, user, showSidebar } from '$lib/stores';
|
import { WEBUI_NAME, config, user as _user, showSidebar } from '$lib/stores';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { onMount, getContext } from 'svelte';
|
import { onMount, getContext } from 'svelte';
|
||||||
|
|
||||||
|
|
@ -14,29 +14,21 @@
|
||||||
import { getChannelMembersById } from '$lib/apis/channels';
|
import { getChannelMembersById } from '$lib/apis/channels';
|
||||||
|
|
||||||
import Pagination from '$lib/components/common/Pagination.svelte';
|
import Pagination from '$lib/components/common/Pagination.svelte';
|
||||||
import ChatBubbles from '$lib/components/icons/ChatBubbles.svelte';
|
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
|
||||||
import EditUserModal from '$lib/components/admin/Users/UserList/EditUserModal.svelte';
|
|
||||||
import UserChatsModal from '$lib/components/admin/Users/UserList/UserChatsModal.svelte';
|
|
||||||
import AddUserModal from '$lib/components/admin/Users/UserList/AddUserModal.svelte';
|
|
||||||
|
|
||||||
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
|
||||||
import RoleUpdateConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
|
||||||
|
|
||||||
import Badge from '$lib/components/common/Badge.svelte';
|
import Badge from '$lib/components/common/Badge.svelte';
|
||||||
import Plus from '$lib/components/icons/Plus.svelte';
|
import Plus from '$lib/components/icons/Plus.svelte';
|
||||||
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
|
||||||
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
|
||||||
import About from '$lib/components/chat/Settings/About.svelte';
|
|
||||||
import Banner from '$lib/components/common/Banner.svelte';
|
|
||||||
import Markdown from '$lib/components/chat/Messages/Markdown.svelte';
|
|
||||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
import ProfilePreview from '../Messages/Message/ProfilePreview.svelte';
|
import ProfilePreview from '../Messages/Message/ProfilePreview.svelte';
|
||||||
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let channel = null;
|
export let channel = null;
|
||||||
|
|
||||||
|
export let onAdd = null;
|
||||||
|
export let onRemove = null;
|
||||||
|
|
||||||
export let search = true;
|
export let search = true;
|
||||||
export let sort = true;
|
export let sort = true;
|
||||||
|
|
||||||
|
|
@ -85,7 +77,13 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$: if (page !== null && query !== null && orderBy !== null && direction !== null) {
|
$: if (
|
||||||
|
channel !== null &&
|
||||||
|
page !== null &&
|
||||||
|
query !== null &&
|
||||||
|
orderBy !== null &&
|
||||||
|
direction !== null
|
||||||
|
) {
|
||||||
getUserList();
|
getUserList();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -96,10 +94,33 @@
|
||||||
<Spinner className="size-5" />
|
<Spinner className="size-5" />
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
<div class="flex items-center justify-between px-2 mb-1">
|
||||||
|
<div class="flex gap-1 items-center">
|
||||||
|
<span class="text-sm">
|
||||||
|
{$i18n.t('Members')}
|
||||||
|
</span>
|
||||||
|
<span class="text-sm text-gray-500">{total}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if onAdd}
|
||||||
|
<div class="">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class=" px-3 py-1.5 gap-1 rounded-xl bg-black dark:text-white dark:bg-gray-850/50 text-black transition font-medium text-xs flex items-center justify-center"
|
||||||
|
on:click={onAdd}
|
||||||
|
>
|
||||||
|
<Plus className="size-3.5 " />
|
||||||
|
<span>{$i18n.t('Add Member')}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<!-- <hr class="my-1 border-gray-100/5- dark:border-gray-850/50" /> -->
|
||||||
|
|
||||||
{#if search}
|
{#if search}
|
||||||
<div class="flex gap-1 px-0.5">
|
<div class="flex gap-1 px-1 mb-1">
|
||||||
<div class=" flex w-full space-x-2">
|
<div class=" flex w-full space-x-2">
|
||||||
<div class="flex flex-1">
|
<div class="flex flex-1 items-center">
|
||||||
<div class=" self-center ml-1 mr-3">
|
<div class=" self-center ml-1 mr-3">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|
@ -127,7 +148,7 @@
|
||||||
{#if users.length > 0}
|
{#if users.length > 0}
|
||||||
<div class="scrollbar-hidden relative whitespace-nowrap w-full max-w-full">
|
<div class="scrollbar-hidden relative whitespace-nowrap w-full max-w-full">
|
||||||
<div class=" text-sm text-left text-gray-500 dark:text-gray-400 w-full max-w-full">
|
<div class=" text-sm text-left text-gray-500 dark:text-gray-400 w-full max-w-full">
|
||||||
<div
|
<!-- <div
|
||||||
class="text-xs text-gray-800 uppercase bg-transparent dark:text-gray-200 w-full mb-0.5"
|
class="text-xs text-gray-800 uppercase bg-transparent dark:text-gray-200 w-full mb-0.5"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|
@ -181,11 +202,11 @@
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> -->
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
{#each users as user, userIdx (user.id)}
|
{#each users as user, userIdx (user.id)}
|
||||||
<div class=" dark:border-gray-850 text-xs flex items-center justify-between">
|
<div class=" dark:border-gray-850 text-xs flex items-center justify-between">
|
||||||
<div class="px-3 py-1.5 font-medium text-gray-900 dark:text-white flex-1">
|
<div class="px-2 py-1.5 font-medium text-gray-900 dark:text-white flex-1">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<ProfilePreview {user} side="right" align="center" sideOffset={6}>
|
<ProfilePreview {user} side="right" align="center" sideOffset={6}>
|
||||||
<img
|
<img
|
||||||
|
|
@ -212,8 +233,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="px-3 py-1">
|
<div class="px-2 py-1 flex items-center gap-1 translate-y-0.5">
|
||||||
<div class=" translate-y-0.5">
|
<div class=" ">
|
||||||
<Badge
|
<Badge
|
||||||
type={user.role === 'admin'
|
type={user.role === 'admin'
|
||||||
? 'info'
|
? 'info'
|
||||||
|
|
@ -223,6 +244,21 @@
|
||||||
content={$i18n.t(user.role)}
|
content={$i18n.t(user.role)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if onRemove}
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
class=" rounded-full p-1 hover:bg-gray-100 dark:hover:bg-gray-850 transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
type="button"
|
||||||
|
disabled={user.id === $_user?.id}
|
||||||
|
on:click={() => {
|
||||||
|
onRemove(user.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<XMark />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,11 @@
|
||||||
export let channel;
|
export let channel;
|
||||||
|
|
||||||
export let onPin = (messageId, pinned) => {};
|
export let onPin = (messageId, pinned) => {};
|
||||||
|
export let onUpdate = () => {};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<PinnedMessagesModal bind:show={showChannelPinnedMessagesModal} {channel} {onPin} />
|
<PinnedMessagesModal bind:show={showChannelPinnedMessagesModal} {channel} {onPin} />
|
||||||
<ChannelInfoModal bind:show={showChannelInfoModal} {channel} />
|
<ChannelInfoModal bind:show={showChannelInfoModal} {channel} {onUpdate} />
|
||||||
<nav class="sticky top-0 z-30 w-full px-1.5 py-1 -mb-8 flex items-center drag-region flex flex-col">
|
<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"
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,6 @@
|
||||||
import Checkbox from '$lib/components/common/Checkbox.svelte';
|
import Checkbox from '$lib/components/common/Checkbox.svelte';
|
||||||
import { getGroups } from '$lib/apis/groups';
|
import { getGroups } from '$lib/apis/groups';
|
||||||
|
|
||||||
export let onChange: Function = () => {};
|
|
||||||
|
|
||||||
export let includeGroups = true;
|
export let includeGroups = true;
|
||||||
export let pagination = false;
|
export let pagination = false;
|
||||||
|
|
||||||
|
|
@ -33,6 +31,7 @@
|
||||||
? groups.filter((group) => group.name.toLowerCase().includes(query.toLowerCase()))
|
? groups.filter((group) => group.name.toLowerCase().includes(query.toLowerCase()))
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
let selectedGroup = {};
|
||||||
let selectedUsers = {};
|
let selectedUsers = {};
|
||||||
|
|
||||||
let page = 1;
|
let page = 1;
|
||||||
|
|
@ -43,15 +42,6 @@
|
||||||
let orderBy = 'name'; // default sort key
|
let orderBy = 'name'; // default sort key
|
||||||
let direction = 'asc'; // default sort order
|
let direction = 'asc'; // default sort order
|
||||||
|
|
||||||
const setSortKey = (key) => {
|
|
||||||
if (orderBy === key) {
|
|
||||||
direction = direction === 'asc' ? 'desc' : 'asc';
|
|
||||||
} else {
|
|
||||||
orderBy = key;
|
|
||||||
direction = 'asc';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getUserList = async () => {
|
const getUserList = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await searchUsers(localStorage.token, query, orderBy, direction, page).catch(
|
const res = await searchUsers(localStorage.token, query, orderBy, direction, page).catch(
|
||||||
|
|
@ -96,6 +86,38 @@
|
||||||
<Spinner className="size-5" />
|
<Spinner className="size-5" />
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
{#if groupIds.length > 0}
|
||||||
|
<div class="mx-1 mb-1.5">
|
||||||
|
<div class="text-xs text-gray-500 mx-0.5 mb-1">
|
||||||
|
{groupIds.length}
|
||||||
|
{$i18n.t('groups')}
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-1 flex-wrap">
|
||||||
|
{#each groupIds as id}
|
||||||
|
{#if selectedGroup[id]}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center space-x-1 px-2 py-1 bg-gray-100/50 dark:bg-gray-850 rounded-lg text-xs"
|
||||||
|
on:click={() => {
|
||||||
|
groupIds = groupIds.filter((gid) => gid !== id);
|
||||||
|
delete selectedGroup[id];
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{selectedGroup[id].name}
|
||||||
|
<span class="text-xs text-gray-500">{selectedGroup[id].member_count}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<XMark className="size-3" />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if userIds.length > 0}
|
{#if userIds.length > 0}
|
||||||
<div class="mx-1 mb-1.5">
|
<div class="mx-1 mb-1.5">
|
||||||
<div class="text-xs text-gray-500 mx-0.5 mb-1">
|
<div class="text-xs text-gray-500 mx-0.5 mb-1">
|
||||||
|
|
@ -127,7 +149,7 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="flex gap-1 -mx-0.5 my-1.5">
|
<div class="flex gap-1 mb-1">
|
||||||
<div class=" flex w-full space-x-2">
|
<div class=" flex w-full space-x-2">
|
||||||
<div class="flex flex-1">
|
<div class="flex flex-1">
|
||||||
<div class=" self-center ml-1 mr-3">
|
<div class=" self-center ml-1 mr-3">
|
||||||
|
|
@ -170,10 +192,11 @@
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
if ((groupIds ?? []).includes(group.id)) {
|
if ((groupIds ?? []).includes(group.id)) {
|
||||||
groupIds = groupIds.filter((id) => id !== group.id);
|
groupIds = groupIds.filter((id) => id !== group.id);
|
||||||
|
delete selectedGroup[group.id];
|
||||||
} else {
|
} else {
|
||||||
groupIds = [...groupIds, group.id];
|
groupIds = [...groupIds, group.id];
|
||||||
|
selectedGroup[group.id] = group;
|
||||||
}
|
}
|
||||||
onChange(groupIds);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="px-3 py-1.5 font-medium text-gray-900 dark:text-white flex-1">
|
<div class="px-3 py-1.5 font-medium text-gray-900 dark:text-white flex-1">
|
||||||
|
|
@ -216,7 +239,6 @@
|
||||||
userIds = [...userIds, user.id];
|
userIds = [...userIds, user.id];
|
||||||
selectedUsers[user.id] = user;
|
selectedUsers[user.id] = user;
|
||||||
}
|
}
|
||||||
onChange(userIds);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="px-3 py-1.5 font-medium text-gray-900 dark:text-white flex-1">
|
<div class="px-3 py-1.5 font-medium text-gray-900 dark:text-white flex-1">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue