Spaces:
Running
Running
Commit ·
fe7c2ba
1
Parent(s): 4d613a5
update phần avatar, update phần create space
Browse files- src/components/ChatArea.jsx +13 -7
- src/components/chatarea/ChatHeader.jsx +1 -1
- src/components/chatarea/ChatMessages.jsx +6 -6
- src/components/chatarea/MentionSuggestions.jsx +1 -1
- src/components/createspace/CreateSpace.jsx +206 -0
- src/components/memberlist/DMProfile.jsx +4 -12
- src/components/memberlist/MemberItem.jsx +1 -1
- src/components/memberlist/UserProfilePopup.jsx +1 -1
- src/components/roomlist/DMList.jsx +3 -3
- src/components/settings/UserProfile.jsx +2 -2
src/components/ChatArea.jsx
CHANGED
|
@@ -244,10 +244,15 @@ function ChatArea({ activeView, activeRoom }) {
|
|
| 244 |
// Convert API messages to UI format
|
| 245 |
const apiMessages = sortedDmMessages.map((msg) => {
|
| 246 |
const isOwn = msg.sender_id === currentUser?.id;
|
| 247 |
-
const sender = isOwn
|
|
|
|
|
|
|
| 248 |
const avatar = isOwn
|
| 249 |
-
? (currentUser?.name?.charAt(0).toUpperCase() || "
|
| 250 |
: (msg.sender?.display_name?.charAt(0).toUpperCase() || "?");
|
|
|
|
|
|
|
|
|
|
| 251 |
|
| 252 |
const date = new Date(msg.created_at);
|
| 253 |
const timestamp = isNaN(date.getTime())
|
|
@@ -258,6 +263,7 @@ function ChatArea({ activeView, activeRoom }) {
|
|
| 258 |
id: msg.id,
|
| 259 |
sender,
|
| 260 |
avatar,
|
|
|
|
| 261 |
timestamp,
|
| 262 |
content: msg.content,
|
| 263 |
isPinned: false,
|
|
@@ -323,8 +329,8 @@ function ChatArea({ activeView, activeRoom }) {
|
|
| 323 |
const tempId = `temp-${Date.now()}`;
|
| 324 |
const optimisticMsg = {
|
| 325 |
id: tempId,
|
| 326 |
-
sender: "
|
| 327 |
-
avatar: currentUser?.name?.charAt(0).toUpperCase() || "
|
| 328 |
timestamp: new Date().toLocaleTimeString("vi-VN", {
|
| 329 |
hour: "2-digit",
|
| 330 |
minute: "2-digit",
|
|
@@ -357,7 +363,7 @@ function ChatArea({ activeView, activeRoom }) {
|
|
| 357 |
created_at: optimisticMsg.created_at,
|
| 358 |
sender: {
|
| 359 |
id: currentUser?.id,
|
| 360 |
-
display_name: currentUser?.name || "
|
| 361 |
avatar_url: currentUser?.avatar || null,
|
| 362 |
},
|
| 363 |
pending: true,
|
|
@@ -458,9 +464,9 @@ function ChatArea({ activeView, activeRoom }) {
|
|
| 458 |
dispatch(cancelReply());
|
| 459 |
}}
|
| 460 |
onShowProfile={(senderName) => {
|
| 461 |
-
if (isDM && dmUser && senderName !==
|
| 462 |
dispatch(setSelectedUser(dmUser));
|
| 463 |
-
} else if (senderName !==
|
| 464 |
dispatch(
|
| 465 |
setSelectedUser({
|
| 466 |
id: senderName.toLowerCase(),
|
|
|
|
| 244 |
// Convert API messages to UI format
|
| 245 |
const apiMessages = sortedDmMessages.map((msg) => {
|
| 246 |
const isOwn = msg.sender_id === currentUser?.id;
|
| 247 |
+
const sender = isOwn
|
| 248 |
+
? (currentUser?.display_name || currentUser?.name || "Bạn")
|
| 249 |
+
: (msg.sender?.display_name || "Unknown");
|
| 250 |
const avatar = isOwn
|
| 251 |
+
? (currentUser?.display_name?.charAt(0).toUpperCase() || currentUser?.name?.charAt(0).toUpperCase() || "B")
|
| 252 |
: (msg.sender?.display_name?.charAt(0).toUpperCase() || "?");
|
| 253 |
+
const color = isOwn
|
| 254 |
+
? (currentUser?.color || null)
|
| 255 |
+
: (msg.sender?.color || null);
|
| 256 |
|
| 257 |
const date = new Date(msg.created_at);
|
| 258 |
const timestamp = isNaN(date.getTime())
|
|
|
|
| 263 |
id: msg.id,
|
| 264 |
sender,
|
| 265 |
avatar,
|
| 266 |
+
color,
|
| 267 |
timestamp,
|
| 268 |
content: msg.content,
|
| 269 |
isPinned: false,
|
|
|
|
| 329 |
const tempId = `temp-${Date.now()}`;
|
| 330 |
const optimisticMsg = {
|
| 331 |
id: tempId,
|
| 332 |
+
sender: currentUser?.display_name || currentUser?.name || "Bạn",
|
| 333 |
+
avatar: currentUser?.display_name?.charAt(0).toUpperCase() || currentUser?.name?.charAt(0).toUpperCase() || "B",
|
| 334 |
timestamp: new Date().toLocaleTimeString("vi-VN", {
|
| 335 |
hour: "2-digit",
|
| 336 |
minute: "2-digit",
|
|
|
|
| 363 |
created_at: optimisticMsg.created_at,
|
| 364 |
sender: {
|
| 365 |
id: currentUser?.id,
|
| 366 |
+
display_name: currentUser?.display_name || currentUser?.name || "Bạn",
|
| 367 |
avatar_url: currentUser?.avatar || null,
|
| 368 |
},
|
| 369 |
pending: true,
|
|
|
|
| 464 |
dispatch(cancelReply());
|
| 465 |
}}
|
| 466 |
onShowProfile={(senderName) => {
|
| 467 |
+
if (isDM && dmUser && senderName !== (currentUser?.display_name || currentUser?.name)) {
|
| 468 |
dispatch(setSelectedUser(dmUser));
|
| 469 |
+
} else if (senderName !== (currentUser?.display_name || currentUser?.name)) {
|
| 470 |
dispatch(
|
| 471 |
setSelectedUser({
|
| 472 |
id: senderName.toLowerCase(),
|
src/components/chatarea/ChatHeader.jsx
CHANGED
|
@@ -31,7 +31,7 @@ function UserAvatar({ name, avatarUrl, isOnline, isDark, isBot, color }) {
|
|
| 31 |
return (
|
| 32 |
<div className="relative flex-shrink-0">
|
| 33 |
<div
|
| 34 |
-
className="w-9 h-9 rounded-
|
| 35 |
style={{
|
| 36 |
background: userColor,
|
| 37 |
color: textColor,
|
|
|
|
| 31 |
return (
|
| 32 |
<div className="relative flex-shrink-0">
|
| 33 |
<div
|
| 34 |
+
className="w-9 h-9 rounded-lg flex items-center justify-center text-sm font-semibold overflow-hidden"
|
| 35 |
style={{
|
| 36 |
background: userColor,
|
| 37 |
color: textColor,
|
src/components/chatarea/ChatMessages.jsx
CHANGED
|
@@ -13,11 +13,11 @@ function ChatMessage({
|
|
| 13 |
onShowProfile,
|
| 14 |
isSending,
|
| 15 |
}) {
|
| 16 |
-
const senderColor = getUserColor(msg.sender);
|
| 17 |
const [showActions, setShowActions] = useState(false);
|
| 18 |
const [reactions, setReactions] = useState(msg.reactions || []);
|
| 19 |
const [showPicker, setShowPicker] = useState(false);
|
| 20 |
-
const isOwnMessage = msg.
|
| 21 |
|
| 22 |
const handleAddReaction = (emoji) => {
|
| 23 |
const existing = reactions.find((r) => r.emoji === emoji);
|
|
@@ -59,7 +59,7 @@ function ChatMessage({
|
|
| 59 |
/>
|
| 60 |
|
| 61 |
<div
|
| 62 |
-
className="w-9 h-9 rounded-
|
| 63 |
style={{
|
| 64 |
background: msg.isBot ? "var(--tertiary-active)" : senderColor,
|
| 65 |
color: msg.isBot
|
|
@@ -166,7 +166,7 @@ function TypingIndicator({ isDark }) {
|
|
| 166 |
return (
|
| 167 |
<div className="flex gap-3 px-3 py-2">
|
| 168 |
<div
|
| 169 |
-
className="w-9 h-9 rounded-
|
| 170 |
style={{
|
| 171 |
background: "var(--tertiary-active)",
|
| 172 |
color: "var(--tertiary)",
|
|
@@ -205,7 +205,7 @@ function MessageSkeleton({ isDark, width = "75%", showSecondLine = true }) {
|
|
| 205 |
return (
|
| 206 |
<div className="flex gap-3 px-3 py-3 animate-pulse">
|
| 207 |
<div
|
| 208 |
-
className="w-9 h-9 rounded-
|
| 209 |
style={{
|
| 210 |
background: isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.08)",
|
| 211 |
}}
|
|
@@ -254,7 +254,7 @@ function EmptyChatState({ dmUser, isDark, hasNoSelection }) {
|
|
| 254 |
return (
|
| 255 |
<div className="flex flex-col items-center justify-center px-6 text-center">
|
| 256 |
<div
|
| 257 |
-
className="w-20 h-20 rounded-
|
| 258 |
style={{
|
| 259 |
background: isDark ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.03)",
|
| 260 |
}}
|
|
|
|
| 13 |
onShowProfile,
|
| 14 |
isSending,
|
| 15 |
}) {
|
| 16 |
+
const senderColor = getUserColor(msg.sender, msg.color);
|
| 17 |
const [showActions, setShowActions] = useState(false);
|
| 18 |
const [reactions, setReactions] = useState(msg.reactions || []);
|
| 19 |
const [showPicker, setShowPicker] = useState(false);
|
| 20 |
+
const isOwnMessage = msg.isOwn;
|
| 21 |
|
| 22 |
const handleAddReaction = (emoji) => {
|
| 23 |
const existing = reactions.find((r) => r.emoji === emoji);
|
|
|
|
| 59 |
/>
|
| 60 |
|
| 61 |
<div
|
| 62 |
+
className="w-9 h-9 rounded-lg flex items-center justify-center text-sm font-semibold shrink-0 cursor-pointer"
|
| 63 |
style={{
|
| 64 |
background: msg.isBot ? "var(--tertiary-active)" : senderColor,
|
| 65 |
color: msg.isBot
|
|
|
|
| 166 |
return (
|
| 167 |
<div className="flex gap-3 px-3 py-2">
|
| 168 |
<div
|
| 169 |
+
className="w-9 h-9 rounded-lg flex items-center justify-center text-sm font-semibold shrink-0"
|
| 170 |
style={{
|
| 171 |
background: "var(--tertiary-active)",
|
| 172 |
color: "var(--tertiary)",
|
|
|
|
| 205 |
return (
|
| 206 |
<div className="flex gap-3 px-3 py-3 animate-pulse">
|
| 207 |
<div
|
| 208 |
+
className="w-9 h-9 rounded-lg flex-shrink-0"
|
| 209 |
style={{
|
| 210 |
background: isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.08)",
|
| 211 |
}}
|
|
|
|
| 254 |
return (
|
| 255 |
<div className="flex flex-col items-center justify-center px-6 text-center">
|
| 256 |
<div
|
| 257 |
+
className="w-20 h-20 rounded-lg flex items-center justify-center mb-5"
|
| 258 |
style={{
|
| 259 |
background: isDark ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.03)",
|
| 260 |
}}
|
src/components/chatarea/MentionSuggestions.jsx
CHANGED
|
@@ -85,7 +85,7 @@ function MentionSuggestions({
|
|
| 85 |
onMouseEnter={() => setSelectedIndex(index)}
|
| 86 |
>
|
| 87 |
<div
|
| 88 |
-
className="w-7 h-7 rounded-
|
| 89 |
style={{
|
| 90 |
background: user.isBot
|
| 91 |
? "var(--tertiary-active)"
|
|
|
|
| 85 |
onMouseEnter={() => setSelectedIndex(index)}
|
| 86 |
>
|
| 87 |
<div
|
| 88 |
+
className="w-7 h-7 rounded-lg flex items-center justify-center text-xs font-semibold flex-shrink-0"
|
| 89 |
style={{
|
| 90 |
background: user.isBot
|
| 91 |
? "var(--tertiary-active)"
|
src/components/createspace/CreateSpace.jsx
CHANGED
|
@@ -43,7 +43,9 @@ import {
|
|
| 43 |
PiFire,
|
| 44 |
PiSnowflake,
|
| 45 |
} from "react-icons/pi";
|
|
|
|
| 46 |
import { cancelCreateSpace } from "../../store/slices/appSlice";
|
|
|
|
| 47 |
|
| 48 |
const spaceIcons = [
|
| 49 |
{ id: "graduation", icon: PiGraduationCap, label: "Tốt nghiệp" },
|
|
@@ -95,6 +97,64 @@ function CreateSpace() {
|
|
| 95 |
const [spaceName, setSpaceName] = useState("");
|
| 96 |
const [spaceIcon, setSpaceIcon] = useState(spaceIcons[0].id);
|
| 97 |
const [spaceDescription, setSpaceDescription] = useState("");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
|
| 99 |
const handleSubmit = () => {
|
| 100 |
if (spaceName.trim()) {
|
|
@@ -106,6 +166,7 @@ function CreateSpace() {
|
|
| 106 |
name: spaceName.trim(),
|
| 107 |
icon: spaceIcon,
|
| 108 |
description: spaceDescription.trim(),
|
|
|
|
| 109 |
hasNotification: false,
|
| 110 |
};
|
| 111 |
console.log("Creating space:", newSpace);
|
|
@@ -241,6 +302,151 @@ function CreateSpace() {
|
|
| 241 |
/>
|
| 242 |
</div>
|
| 243 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
{/* Preview */}
|
| 245 |
{spaceName && (
|
| 246 |
<div>
|
|
|
|
| 43 |
PiFire,
|
| 44 |
PiSnowflake,
|
| 45 |
} from "react-icons/pi";
|
| 46 |
+
import { FiSearch, FiX } from "react-icons/fi";
|
| 47 |
import { cancelCreateSpace } from "../../store/slices/appSlice";
|
| 48 |
+
import { dmService } from "../../services/dm.service";
|
| 49 |
|
| 50 |
const spaceIcons = [
|
| 51 |
{ id: "graduation", icon: PiGraduationCap, label: "Tốt nghiệp" },
|
|
|
|
| 97 |
const [spaceName, setSpaceName] = useState("");
|
| 98 |
const [spaceIcon, setSpaceIcon] = useState(spaceIcons[0].id);
|
| 99 |
const [spaceDescription, setSpaceDescription] = useState("");
|
| 100 |
+
|
| 101 |
+
// Members state
|
| 102 |
+
const [members, setMembers] = useState([]);
|
| 103 |
+
const [searchQuery, setSearchQuery] = useState("");
|
| 104 |
+
const [searchResults, setSearchResults] = useState([]);
|
| 105 |
+
const [isSearching, setIsSearching] = useState(false);
|
| 106 |
+
const searchTimeoutRef = useState(null);
|
| 107 |
+
|
| 108 |
+
// Search users - only trigger on Enter key
|
| 109 |
+
const handleSearch = async (query) => {
|
| 110 |
+
setSearchQuery(query);
|
| 111 |
+
|
| 112 |
+
if (!query.trim()) {
|
| 113 |
+
setSearchResults([]);
|
| 114 |
+
setIsSearching(false);
|
| 115 |
+
return;
|
| 116 |
+
}
|
| 117 |
+
};
|
| 118 |
+
|
| 119 |
+
const handleSearchKeyDown = async (e) => {
|
| 120 |
+
if (e.key === 'Enter') {
|
| 121 |
+
e.preventDefault();
|
| 122 |
+
const query = searchQuery.trim();
|
| 123 |
+
|
| 124 |
+
if (!query) {
|
| 125 |
+
setSearchResults([]);
|
| 126 |
+
return;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
setIsSearching(true);
|
| 130 |
+
try {
|
| 131 |
+
const { data } = await dmService.searchUsers(query);
|
| 132 |
+
const users = (data.users || []).map((user) => ({
|
| 133 |
+
id: user.id,
|
| 134 |
+
name: user.display_name || user.email || "Unknown",
|
| 135 |
+
avatar: user.avatar_url || null,
|
| 136 |
+
email: user.email || "",
|
| 137 |
+
}));
|
| 138 |
+
setSearchResults(users);
|
| 139 |
+
} catch {
|
| 140 |
+
setSearchResults([]);
|
| 141 |
+
} finally {
|
| 142 |
+
setIsSearching(false);
|
| 143 |
+
}
|
| 144 |
+
}
|
| 145 |
+
};
|
| 146 |
+
|
| 147 |
+
// Add member
|
| 148 |
+
const addMember = (user) => {
|
| 149 |
+
if (!members.some((m) => m.id === user.id)) {
|
| 150 |
+
setMembers([...members, user]);
|
| 151 |
+
}
|
| 152 |
+
};
|
| 153 |
+
|
| 154 |
+
// Remove member
|
| 155 |
+
const removeMember = (userId) => {
|
| 156 |
+
setMembers((prev) => prev.filter((m) => m.id !== userId));
|
| 157 |
+
};
|
| 158 |
|
| 159 |
const handleSubmit = () => {
|
| 160 |
if (spaceName.trim()) {
|
|
|
|
| 166 |
name: spaceName.trim(),
|
| 167 |
icon: spaceIcon,
|
| 168 |
description: spaceDescription.trim(),
|
| 169 |
+
members: members.map((m) => m.id),
|
| 170 |
hasNotification: false,
|
| 171 |
};
|
| 172 |
console.log("Creating space:", newSpace);
|
|
|
|
| 302 |
/>
|
| 303 |
</div>
|
| 304 |
|
| 305 |
+
{/* Members */}
|
| 306 |
+
<div>
|
| 307 |
+
<h3
|
| 308 |
+
className="text-sm font-semibold mb-3"
|
| 309 |
+
style={{ color: "var(--text-primary)" }}
|
| 310 |
+
>
|
| 311 |
+
Thêm thành viên
|
| 312 |
+
</h3>
|
| 313 |
+
|
| 314 |
+
{/* Search input */}
|
| 315 |
+
<div className="relative mb-3">
|
| 316 |
+
{isSearching ? (
|
| 317 |
+
<div className="absolute left-3 top-1/2 -translate-y-1/2">
|
| 318 |
+
<div className="w-4 h-4 border-2 border-t-transparent rounded-full animate-spin"
|
| 319 |
+
style={{ borderColor: "var(--text-muted)", borderTopColor: "transparent" }}
|
| 320 |
+
/>
|
| 321 |
+
</div>
|
| 322 |
+
) : (
|
| 323 |
+
<button
|
| 324 |
+
onClick={() => handleSearchKeyDown({ key: 'Enter', preventDefault: () => {} })}
|
| 325 |
+
className="absolute left-3 top-1/2 -translate-y-1/2 hover:opacity-70 transition-opacity"
|
| 326 |
+
style={{ color: "var(--text-muted)" }}
|
| 327 |
+
>
|
| 328 |
+
<FiSearch size={16} />
|
| 329 |
+
</button>
|
| 330 |
+
)}
|
| 331 |
+
<input
|
| 332 |
+
type="text"
|
| 333 |
+
value={searchQuery}
|
| 334 |
+
onChange={(e) => handleSearch(e.target.value)}
|
| 335 |
+
onKeyDown={handleSearchKeyDown}
|
| 336 |
+
placeholder="Nhập tên và nhấn Enter để tìm..."
|
| 337 |
+
className="w-full pl-9 pr-3 py-2 rounded-md text-sm border outline-none"
|
| 338 |
+
style={{
|
| 339 |
+
background: "var(--input-bg)",
|
| 340 |
+
borderColor: "var(--input-border)",
|
| 341 |
+
color: "var(--input-text)",
|
| 342 |
+
}}
|
| 343 |
+
onFocus={(e) =>
|
| 344 |
+
(e.currentTarget.style.borderColor = "var(--primary)")
|
| 345 |
+
}
|
| 346 |
+
onBlur={(e) =>
|
| 347 |
+
(e.currentTarget.style.borderColor = "var(--input-border)")
|
| 348 |
+
}
|
| 349 |
+
/>
|
| 350 |
+
</div>
|
| 351 |
+
|
| 352 |
+
{/* Selected members - chips */}
|
| 353 |
+
{members.length > 0 && (
|
| 354 |
+
<div className="mb-3">
|
| 355 |
+
<div
|
| 356 |
+
className="text-xs font-medium mb-2"
|
| 357 |
+
style={{ color: "var(--text-secondary)" }}
|
| 358 |
+
>
|
| 359 |
+
Đã chọn ({members.length})
|
| 360 |
+
</div>
|
| 361 |
+
<div className="flex flex-wrap gap-2">
|
| 362 |
+
{members.map((member) => (
|
| 363 |
+
<div
|
| 364 |
+
key={member.id}
|
| 365 |
+
className="flex items-center gap-2 px-3 py-1.5 rounded-full text-sm"
|
| 366 |
+
style={{
|
| 367 |
+
background: "var(--primary-active)",
|
| 368 |
+
color: "var(--primary)",
|
| 369 |
+
}}
|
| 370 |
+
>
|
| 371 |
+
<div
|
| 372 |
+
className="w-5 h-5 rounded-full flex items-center justify-center text-[10px] font-semibold"
|
| 373 |
+
style={{
|
| 374 |
+
background: "var(--primary)",
|
| 375 |
+
color: "#fff",
|
| 376 |
+
}}
|
| 377 |
+
>
|
| 378 |
+
{member.name?.charAt(0)?.toUpperCase() || "?"}
|
| 379 |
+
</div>
|
| 380 |
+
<span className="font-medium">{member.name}</span>
|
| 381 |
+
<button
|
| 382 |
+
onClick={() => removeMember(member.id)}
|
| 383 |
+
className="ml-1 hover:opacity-70"
|
| 384 |
+
>
|
| 385 |
+
<FiX size={14} />
|
| 386 |
+
</button>
|
| 387 |
+
</div>
|
| 388 |
+
))}
|
| 389 |
+
</div>
|
| 390 |
+
</div>
|
| 391 |
+
)}
|
| 392 |
+
|
| 393 |
+
{/* Search results with checkbox - keep showing after select */}
|
| 394 |
+
{searchResults.length > 0 && (
|
| 395 |
+
<div>
|
| 396 |
+
<div
|
| 397 |
+
className="text-xs font-medium mb-2"
|
| 398 |
+
style={{ color: "var(--text-secondary)" }}
|
| 399 |
+
>
|
| 400 |
+
Kết quả tìm kiếm
|
| 401 |
+
</div>
|
| 402 |
+
<div className="space-y-1">
|
| 403 |
+
{searchResults.map((user) => {
|
| 404 |
+
const isSelected = members.some((m) => m.id === user.id);
|
| 405 |
+
return (
|
| 406 |
+
<label
|
| 407 |
+
key={user.id}
|
| 408 |
+
className="flex items-center gap-3 px-3 py-2 rounded-md cursor-pointer transition-colors hover:opacity-80"
|
| 409 |
+
>
|
| 410 |
+
<div
|
| 411 |
+
className="w-8 h-8 rounded-full flex items-center justify-center text-xs font-semibold"
|
| 412 |
+
style={{
|
| 413 |
+
background: "var(--primary-active)",
|
| 414 |
+
color: "var(--primary)",
|
| 415 |
+
}}
|
| 416 |
+
>
|
| 417 |
+
{user.name?.charAt(0)?.toUpperCase() || "?"}
|
| 418 |
+
</div>
|
| 419 |
+
<div className="flex-1 min-w-0">
|
| 420 |
+
<div
|
| 421 |
+
className="text-sm font-medium truncate"
|
| 422 |
+
style={{ color: "var(--text-primary)" }}
|
| 423 |
+
>
|
| 424 |
+
{user.name}
|
| 425 |
+
</div>
|
| 426 |
+
</div>
|
| 427 |
+
<input
|
| 428 |
+
type="checkbox"
|
| 429 |
+
checked={isSelected}
|
| 430 |
+
onChange={() => {
|
| 431 |
+
if (isSelected) {
|
| 432 |
+
removeMember(user.id);
|
| 433 |
+
} else {
|
| 434 |
+
addMember(user);
|
| 435 |
+
}
|
| 436 |
+
}}
|
| 437 |
+
className="w-4 h-4 cursor-pointer"
|
| 438 |
+
style={{
|
| 439 |
+
accentColor: "var(--primary)",
|
| 440 |
+
}}
|
| 441 |
+
/>
|
| 442 |
+
</label>
|
| 443 |
+
);
|
| 444 |
+
})}
|
| 445 |
+
</div>
|
| 446 |
+
</div>
|
| 447 |
+
)}
|
| 448 |
+
</div>
|
| 449 |
+
|
| 450 |
{/* Preview */}
|
| 451 |
{spaceName && (
|
| 452 |
<div>
|
src/components/memberlist/DMProfile.jsx
CHANGED
|
@@ -41,7 +41,7 @@ function UserAvatar({ name, avatarUrl, isOnline, isDark, isBot, color }) {
|
|
| 41 |
return (
|
| 42 |
<div className="relative shrink-0">
|
| 43 |
<div
|
| 44 |
-
className="w-16 h-16 rounded-
|
| 45 |
style={{
|
| 46 |
background: userColor,
|
| 47 |
color: textColor,
|
|
@@ -60,15 +60,6 @@ function UserAvatar({ name, avatarUrl, isOnline, isDark, isBot, color }) {
|
|
| 60 |
getInitials(name)
|
| 61 |
)}
|
| 62 |
</div>
|
| 63 |
-
{!isBot && (
|
| 64 |
-
<div
|
| 65 |
-
className="absolute bottom-0.5 right-0.5 w-3 h-3 rounded-full border-2"
|
| 66 |
-
style={{
|
| 67 |
-
borderColor: isDark ? "var(--bg-surface-secondary)" : "#fff",
|
| 68 |
-
background: isOnline ? "var(--online)" : "var(--offline)",
|
| 69 |
-
}}
|
| 70 |
-
/>
|
| 71 |
-
)}
|
| 72 |
</div>
|
| 73 |
);
|
| 74 |
}
|
|
@@ -90,7 +81,8 @@ function DMProfile({ isDark, dmUser }) {
|
|
| 90 |
.getUserProfile(dmUser.id)
|
| 91 |
.then(({ data }) => {
|
| 92 |
const color = data?.user?.color || data?.color || null;
|
| 93 |
-
const displayName =
|
|
|
|
| 94 |
const avatarUrl = data?.user?.avatar_url || data?.avatar_url || null;
|
| 95 |
if (mounted && color) {
|
| 96 |
setProfileColor(color);
|
|
@@ -105,7 +97,7 @@ function DMProfile({ isDark, dmUser }) {
|
|
| 105 |
...(displayName && { display_name: displayName }),
|
| 106 |
...(avatarUrl && { avatar_url: avatarUrl }),
|
| 107 |
},
|
| 108 |
-
})
|
| 109 |
);
|
| 110 |
}
|
| 111 |
})
|
|
|
|
| 41 |
return (
|
| 42 |
<div className="relative shrink-0">
|
| 43 |
<div
|
| 44 |
+
className="w-16 h-16 rounded-lg flex items-center justify-center text-2xl font-semibold overflow-hidden"
|
| 45 |
style={{
|
| 46 |
background: userColor,
|
| 47 |
color: textColor,
|
|
|
|
| 60 |
getInitials(name)
|
| 61 |
)}
|
| 62 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
</div>
|
| 64 |
);
|
| 65 |
}
|
|
|
|
| 81 |
.getUserProfile(dmUser.id)
|
| 82 |
.then(({ data }) => {
|
| 83 |
const color = data?.user?.color || data?.color || null;
|
| 84 |
+
const displayName =
|
| 85 |
+
data?.user?.display_name || data?.display_name || null;
|
| 86 |
const avatarUrl = data?.user?.avatar_url || data?.avatar_url || null;
|
| 87 |
if (mounted && color) {
|
| 88 |
setProfileColor(color);
|
|
|
|
| 97 |
...(displayName && { display_name: displayName }),
|
| 98 |
...(avatarUrl && { avatar_url: avatarUrl }),
|
| 99 |
},
|
| 100 |
+
}),
|
| 101 |
);
|
| 102 |
}
|
| 103 |
})
|
src/components/memberlist/MemberItem.jsx
CHANGED
|
@@ -13,7 +13,7 @@ function MemberItem({ isDark, member, onClick }) {
|
|
| 13 |
onClick={() => onClick?.()}
|
| 14 |
>
|
| 15 |
<div
|
| 16 |
-
className="w-8 h-8 rounded-
|
| 17 |
style={{
|
| 18 |
background: member.isBot ? "var(--tertiary-active)" : memberColor,
|
| 19 |
color: member.isBot
|
|
|
|
| 13 |
onClick={() => onClick?.()}
|
| 14 |
>
|
| 15 |
<div
|
| 16 |
+
className="w-8 h-8 rounded-lg flex items-center justify-center text-[13px] font-semibold flex-shrink-0 relative"
|
| 17 |
style={{
|
| 18 |
background: member.isBot ? "var(--tertiary-active)" : memberColor,
|
| 19 |
color: member.isBot
|
src/components/memberlist/UserProfilePopup.jsx
CHANGED
|
@@ -55,7 +55,7 @@ function UserProfilePopup({
|
|
| 55 |
{/* Avatar */}
|
| 56 |
<div className="px-4 -mt-8">
|
| 57 |
<div
|
| 58 |
-
className="w-16 h-16 rounded-
|
| 59 |
style={{
|
| 60 |
background: userColor,
|
| 61 |
color: isDark ? "var(--bg-surface)" : "#fff",
|
|
|
|
| 55 |
{/* Avatar */}
|
| 56 |
<div className="px-4 -mt-8">
|
| 57 |
<div
|
| 58 |
+
className="w-16 h-16 rounded-lg flex items-center justify-center text-2xl font-semibold border-4 relative"
|
| 59 |
style={{
|
| 60 |
background: userColor,
|
| 61 |
color: isDark ? "var(--bg-surface)" : "#fff",
|
src/components/roomlist/DMList.jsx
CHANGED
|
@@ -43,7 +43,7 @@ function UserAvatar({ name, avatarUrl, isOnline, isDark, isBot, color }) {
|
|
| 43 |
return (
|
| 44 |
<div className="relative flex-shrink-0">
|
| 45 |
<div
|
| 46 |
-
className="w-10 h-10 rounded-
|
| 47 |
style={{
|
| 48 |
background: userColor,
|
| 49 |
color: textColor,
|
|
@@ -96,7 +96,7 @@ function NoResultsState({ isDark }) {
|
|
| 96 |
return (
|
| 97 |
<div className="flex flex-col items-center justify-center py-10 px-4 text-center">
|
| 98 |
<div
|
| 99 |
-
className="w-14 h-14 rounded-
|
| 100 |
style={{
|
| 101 |
background: isDark ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.03)",
|
| 102 |
}}
|
|
@@ -120,7 +120,7 @@ function EmptyState({ isDark, onStartChat }) {
|
|
| 120 |
return (
|
| 121 |
<div className="flex flex-col items-center justify-center py-10 px-4 text-center">
|
| 122 |
<div
|
| 123 |
-
className="w-16 h-16 rounded-
|
| 124 |
style={{
|
| 125 |
background: isDark ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.03)",
|
| 126 |
}}
|
|
|
|
| 43 |
return (
|
| 44 |
<div className="relative flex-shrink-0">
|
| 45 |
<div
|
| 46 |
+
className="w-10 h-10 rounded-lg flex items-center justify-center text-sm font-semibold overflow-hidden"
|
| 47 |
style={{
|
| 48 |
background: userColor,
|
| 49 |
color: textColor,
|
|
|
|
| 96 |
return (
|
| 97 |
<div className="flex flex-col items-center justify-center py-10 px-4 text-center">
|
| 98 |
<div
|
| 99 |
+
className="w-14 h-14 rounded-lg flex items-center justify-center mb-3"
|
| 100 |
style={{
|
| 101 |
background: isDark ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.03)",
|
| 102 |
}}
|
|
|
|
| 120 |
return (
|
| 121 |
<div className="flex flex-col items-center justify-center py-10 px-4 text-center">
|
| 122 |
<div
|
| 123 |
+
className="w-16 h-16 rounded-lg flex items-center justify-center mb-4"
|
| 124 |
style={{
|
| 125 |
background: isDark ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.03)",
|
| 126 |
}}
|
src/components/settings/UserProfile.jsx
CHANGED
|
@@ -108,7 +108,7 @@ const UserProfile = forwardRef(function UserProfile(_, ref) {
|
|
| 108 |
>
|
| 109 |
<div className="flex items-center gap-4">
|
| 110 |
<div
|
| 111 |
-
className="w-14 h-14 rounded-
|
| 112 |
style={{
|
| 113 |
background: usernameColors.find((c) => c.value === usernameColor)?.hex || "var(--primary)",
|
| 114 |
color: isDark ? "var(--bg-surface)" : "#fff",
|
|
@@ -198,7 +198,7 @@ const UserProfile = forwardRef(function UserProfile(_, ref) {
|
|
| 198 |
<button
|
| 199 |
key={`${color.value}-${usernameColor}`}
|
| 200 |
onClick={() => handleColorChange(color.value)}
|
| 201 |
-
className="w-8 h-8 rounded-
|
| 202 |
style={{
|
| 203 |
background: color.hex,
|
| 204 |
borderColor:
|
|
|
|
| 108 |
>
|
| 109 |
<div className="flex items-center gap-4">
|
| 110 |
<div
|
| 111 |
+
className="w-14 h-14 rounded-lg flex items-center justify-center text-xl font-semibold"
|
| 112 |
style={{
|
| 113 |
background: usernameColors.find((c) => c.value === usernameColor)?.hex || "var(--primary)",
|
| 114 |
color: isDark ? "var(--bg-surface)" : "#fff",
|
|
|
|
| 198 |
<button
|
| 199 |
key={`${color.value}-${usernameColor}`}
|
| 200 |
onClick={() => handleColorChange(color.value)}
|
| 201 |
+
className="w-8 h-8 rounded-lg flex items-center justify-center border-2 transition-all"
|
| 202 |
style={{
|
| 203 |
background: color.hex,
|
| 204 |
borderColor:
|