feat/enh: add/remove users from group channel

This commit is contained in:
Timothy Jaeryang Baek 2025-11-30 10:33:50 -05:00
parent 781aeebd2a
commit 3f1d9ccbf8
11 changed files with 625 additions and 103 deletions

View file

@ -5,7 +5,6 @@ from typing import Optional
from open_webui.internal.db import Base, get_db
from open_webui.models.groups import Groups
from open_webui.utils.access_control import has_access
from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Boolean, Column, String, Text, JSON, case
@ -175,7 +174,9 @@ class ChannelWebhookModel(BaseModel):
class ChannelResponse(ChannelModel):
is_manager: bool = False
write_access: bool = False
user_count: Optional[int] = None
@ -196,32 +197,42 @@ class CreateChannelForm(ChannelForm):
class ChannelTable:
def _create_memberships_by_user_ids_and_group_ids(
def _collect_unique_user_ids(
self,
channel_id: str,
invited_by: str,
user_ids: Optional[list[str]] = None,
group_ids: Optional[list[str]] = None,
) -> list[ChannelMemberModel]:
# For group and direct message channels, automatically add the specified users as members
user_ids = user_ids or []
if invited_by not in user_ids:
user_ids.append(invited_by) # Ensure the creator is also a member
) -> set[str]:
"""
Collect unique user ids from:
- invited_by
- 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
group_ids = group_ids or []
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)
for group_id in group_ids or []:
users.update(Groups.get_group_user_ids_by_id(group_id))
# Ensure uniqueness
user_ids = list(set(user_ids))
return users
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 = []
for uid in user_ids:
channel_member = ChannelMemberModel(
model = ChannelMemberModel(
**{
"id": str(uuid.uuid4()),
"channel_id": channel_id,
@ -230,17 +241,16 @@ class ChannelTable:
"is_active": True,
"is_channel_muted": False,
"is_channel_pinned": False,
"invited_at": int(time.time_ns()),
"invited_at": now,
"invited_by": invited_by,
"joined_at": int(time.time_ns()),
"joined_at": now,
"left_at": None,
"last_read_at": int(time.time_ns()),
"created_at": int(time.time_ns()),
"updated_at": int(time.time_ns()),
"last_read_at": now,
"created_at": now,
"updated_at": now,
}
)
memberships.append(ChannelMember(**channel_member.model_dump()))
memberships.append(ChannelMember(**model.model_dump()))
return memberships
@ -262,14 +272,18 @@ class ChannelTable:
new_channel = Channel(**channel.model_dump())
if form_data.type in ["group", "dm"]:
memberships = self._create_memberships_by_user_ids_and_group_ids(
channel.id,
user_id,
form_data.user_ids,
form_data.group_ids,
users = self._collect_unique_user_ids(
invited_by=user_id,
user_ids=form_data.user_ids,
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.commit()
return channel
@ -279,24 +293,71 @@ class ChannelTable:
channels = db.query(Channel).all()
return [ChannelModel.model_validate(channel) for channel in channels]
def get_channels_by_user_id(
self, user_id: str, permission: str = "read"
) -> list[ChannelModel]:
channels = self.get_channels()
def _has_permission(self, query, filter: dict, permission: str = "read"):
group_ids = filter.get("group_ids", [])
user_id = filter.get("user_id")
channel_list = []
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)
json_group_ids = Channel.access_control[permission]["group_ids"]
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]:
with get_db() as db:
@ -331,6 +392,78 @@ class ChannelTable:
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(
self, channel_id: str, user_id: str
) -> Optional[ChannelMemberModel]:

View file

@ -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.models.chats import Chats
from open_webui.models.groups import Groups, GroupMember
from open_webui.models.channels import ChannelMember
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")
group_ids = filter.get("group_ids")

View file

@ -261,6 +261,7 @@ async def get_channel_by_id(id: str, user=Depends(get_verified_user)):
**channel.model_dump(),
"user_ids": user_ids,
"users": users,
"is_manager": Channels.is_user_channel_manager(channel.id, user.id),
"write_access": True,
"user_count": len(user_ids),
"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(),
"user_ids": user_ids,
"users": users,
"is_manager": Channels.is_user_channel_manager(channel.id, user.id),
"write_access": write_access or user.role == "admin",
"user_count": user_count,
"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()
)
if channel.type == "dm":
user_ids = [
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,
}
else:
filter = {
"roles": ["!pending"],
}
filter = {}
if query:
filter["query"] = query
@ -362,7 +362,13 @@ async def get_channel_members_by_id(
if 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:
filter["user_ids"] = permitted_ids.get("user_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
#################################################
# 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
############################

View file

@ -309,11 +309,13 @@ async def user_join(sid, data):
)
await sio.enter_room(sid, f"user:{user.id}")
# Join all the channels
channels = Channels.get_channels_by_user_id(user.id)
log.debug(f"{channels=}")
for channel in channels:
await sio.enter_room(sid, f"channel:{channel.id}")
return {"id": user.id, "name": user.name}

View file

@ -194,6 +194,88 @@ export const updateChannelMemberActiveStatusById = async (
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 (
token: string = '',
channel_id: string,

View file

@ -309,6 +309,11 @@
return message;
});
}}
onUpdate={async () => {
channel = await getChannelById(localStorage.token, id).catch((error) => {
return null;
});
}}
/>
{#if channel && messages !== null}

View file

@ -3,22 +3,41 @@
import { getContext, onMount } from 'svelte';
const i18n = getContext('i18n');
import { removeMembersById } from '$lib/apis/channels';
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 AddMembersModal from './ChannelInfoModal/AddMembersModal.svelte';
export let show = false;
export let channel = null;
export let onUpdate = () => {};
let showAddMembersModal = false;
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 = () => {};
$: if (show) {
@ -31,15 +50,14 @@
</script>
{#if channel}
<AddMembersModal bind:show={showAddMembersModal} {channel} {onUpdate} />
<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">
{#if channel?.type === 'dm'}
<div
class=" text-left self-center overflow-hidden w-full line-clamp-1 capitalize flex-1"
>
<div class=" text-left self-center overflow-hidden w-full line-clamp-1 flex-1">
{$i18n.t('Direct Message')}
</div>
{:else}
@ -51,9 +69,7 @@
{/if}
</div>
<div
class=" text-left self-center overflow-hidden w-full line-clamp-1 capitalize flex-1"
>
<div class=" text-left self-center overflow-hidden w-full line-clamp-1 flex-1">
{channel.name}
</div>
{/if}
@ -69,7 +85,7 @@
</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 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"
@ -79,7 +95,21 @@
}}
>
<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>
</form>
</div>

View file

@ -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}

View file

@ -1,6 +1,6 @@
<script>
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 { onMount, getContext } from 'svelte';
@ -14,29 +14,21 @@
import { getChannelMembersById } from '$lib/apis/channels';
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 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 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 ProfilePreview from '../Messages/Message/ProfilePreview.svelte';
import XMark from '$lib/components/icons/XMark.svelte';
const i18n = getContext('i18n');
export let channel = null;
export let onAdd = null;
export let onRemove = null;
export let search = 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();
}
</script>
@ -96,10 +94,33 @@
<Spinner className="size-5" />
</div>
{: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}
<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 flex-1">
<div class="flex flex-1 items-center">
<div class=" self-center ml-1 mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
@ -127,7 +148,7 @@
{#if users.length > 0}
<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
<!-- <div
class="text-xs text-gray-800 uppercase bg-transparent dark:text-gray-200 w-full mb-0.5"
>
<div
@ -181,11 +202,11 @@
</div>
</button>
</div>
</div>
</div> -->
<div class="w-full">
{#each users as user, userIdx (user.id)}
<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">
<ProfilePreview {user} side="right" align="center" sideOffset={6}>
<img
@ -212,8 +233,8 @@
</div>
</div>
<div class="px-3 py-1">
<div class=" translate-y-0.5">
<div class="px-2 py-1 flex items-center gap-1 translate-y-0.5">
<div class=" ">
<Badge
type={user.role === 'admin'
? 'info'
@ -223,6 +244,21 @@
content={$i18n.t(user.role)}
/>
</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>
{/each}

View file

@ -29,10 +29,11 @@
export let channel;
export let onPin = (messageId, pinned) => {};
export let onUpdate = () => {};
</script>
<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">
<div
id="navbar-bg-gradient-to-b"

View file

@ -18,8 +18,6 @@
import Checkbox from '$lib/components/common/Checkbox.svelte';
import { getGroups } from '$lib/apis/groups';
export let onChange: Function = () => {};
export let includeGroups = true;
export let pagination = false;
@ -33,6 +31,7 @@
? groups.filter((group) => group.name.toLowerCase().includes(query.toLowerCase()))
: [];
let selectedGroup = {};
let selectedUsers = {};
let page = 1;
@ -43,15 +42,6 @@
let orderBy = 'name'; // default sort key
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 () => {
try {
const res = await searchUsers(localStorage.token, query, orderBy, direction, page).catch(
@ -96,6 +86,38 @@
<Spinner className="size-5" />
</div>
{: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}
<div class="mx-1 mb-1.5">
<div class="text-xs text-gray-500 mx-0.5 mb-1">
@ -127,7 +149,7 @@
</div>
{/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 flex-1">
<div class=" self-center ml-1 mr-3">
@ -170,10 +192,11 @@
on:click={() => {
if ((groupIds ?? []).includes(group.id)) {
groupIds = groupIds.filter((id) => id !== group.id);
delete selectedGroup[group.id];
} else {
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">
@ -216,7 +239,6 @@
userIds = [...userIds, user.id];
selectedUsers[user.id] = user;
}
onChange(userIds);
}}
>
<div class="px-3 py-1.5 font-medium text-gray-900 dark:text-white flex-1">