Skip to content
This repository was archived by the owner on Jan 15, 2025. It is now read-only.

Commit efccd02

Browse files
authored
Merge pull request #1573 from ecency/feature/contact-pinning
Chats: added contacts pinning
2 parents 5d04db4 + a6fb6c2 commit efccd02

File tree

8 files changed

+213
-49
lines changed

8 files changed

+213
-49
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"start:prod": "NODE_ENV=production node build/server.js"
1414
},
1515
"dependencies": {
16-
"@ecency/ns-query": "^1.1.6",
16+
"@ecency/ns-query": "^1.1.7",
1717
"@ecency/render-helper": "^2.2.30",
1818
"@ecency/render-helper-amp": "^1.1.0",
1919
"@emoji-mart/data": "^1.1.2",

src/common/features/chats/components/chat-message-box.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export default function ChatsMessagesBox(props: Props) {
6767
}}
6868
>
6969
<ChatsMessagesHeader
70+
contact={props.currentContact}
7071
channel={props.channel}
7172
username={props.community?.name ?? props.currentContact?.name ?? ""}
7273
history={props.history}

src/common/features/chats/components/chat-messages-header.tsx

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useContext, useMemo } from "react";
1+
import React, { useContext, useEffect, useMemo } from "react";
22
import { History } from "history";
33
import ChatsCommunityDropdownMenu from "./chats-community-actions";
44
import UserAvatar from "../../../components/user-avatar";
@@ -8,16 +8,20 @@ import { Button } from "@ui/button";
88
import {
99
Channel,
1010
ChatContext,
11+
DirectContact,
1112
formattedUserName,
1213
useChannelsQuery,
13-
useKeysQuery
14+
useKeysQuery,
15+
usePinContact
1416
} from "@ecency/ns-query";
1517
import { ChatSidebarSavedMessagesAvatar } from "./chats-sidebar/chat-sidebar-saved-messages-avatar";
1618
import { _t } from "../../../i18n";
19+
import { error, success } from "../../../components/feedback";
1720

1821
interface Props {
1922
username: string;
2023
channel?: Channel;
24+
contact?: DirectContact;
2125
history: History;
2226
}
2327

@@ -30,6 +34,25 @@ export default function ChatsMessagesHeader(props: Props) {
3034

3135
const isActiveUser = useMemo(() => receiverPubKey === publicKey, [publicKey, receiverPubKey]);
3236

37+
const {
38+
mutateAsync: pinContact,
39+
isLoading: isContactPinning,
40+
isSuccess: isPinned,
41+
isError: isPinFailed
42+
} = usePinContact();
43+
44+
useEffect(() => {
45+
if (isPinned) {
46+
success(_t("g.success"));
47+
}
48+
}, [isPinned]);
49+
50+
useEffect(() => {
51+
if (isPinFailed) {
52+
error(_t("g.error"));
53+
}
54+
}, [isPinFailed]);
55+
3356
const formattedName = (username: string) => {
3457
if (username && !username.startsWith("@")) {
3558
const community = channels?.find((channel) => channel.communityName === username);
@@ -70,6 +93,23 @@ export default function ChatsMessagesHeader(props: Props) {
7093
<ChatsCommunityDropdownMenu channel={props.channel} />
7194
</div>
7295
)}
96+
{props.contact && (
97+
<div className="flex items-center justify-center">
98+
<Button
99+
size="sm"
100+
appearance="gray-link"
101+
disabled={isContactPinning}
102+
onClick={(e: { stopPropagation: () => void }) => {
103+
e.stopPropagation();
104+
if (!isContactPinning) {
105+
pinContact({ contact: props.contact!, pinned: !props.contact!.pinned });
106+
}
107+
}}
108+
>
109+
{props.contact.pinned ? _t("chat.unpin") : _t("chat.pin")}
110+
</Button>
111+
</div>
112+
)}
73113
</div>
74114
);
75115
}

src/common/features/chats/components/chat-popup/chat-popup-header.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ export function ChatPopupHeader({
148148
<ChatsDropdownMenu
149149
history={history!}
150150
channel={channel}
151+
contact={directContact}
151152
onManageChatKey={() => setRevealPrivateKey(!revealPrivateKey)}
152153
/>
153154
</div>

src/common/features/chats/components/chats-dropdown-menu.tsx

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,33 @@
1-
import React from "react";
1+
import React, { useEffect } from "react";
22
import { History } from "history";
3-
import { chatKeySvg, chatLeaveSvg, extendedView, kebabMenuSvg, keySvg } from "../../../img/svg";
3+
import {
4+
chatKeySvg,
5+
chatLeaveSvg,
6+
extendedView,
7+
kebabMenuSvg,
8+
keySvg,
9+
pinSvg
10+
} from "../../../img/svg";
411
import { _t } from "../../../i18n";
512
import { Dropdown, DropdownItemWithIcon, DropdownMenu, DropdownToggle } from "@ui/dropdown";
613
import { Button } from "@ui/button";
714
import { history } from "../../../store";
815
import { useLocation } from "react-router";
9-
import { Channel, useLeaveCommunityChannel, useLogoutFromChats } from "@ecency/ns-query";
16+
import {
17+
Channel,
18+
DirectContact,
19+
useLeaveCommunityChannel,
20+
useLogoutFromChats,
21+
usePinContact
22+
} from "@ecency/ns-query";
23+
import { error, success } from "../../../components/feedback";
1024

1125
interface Props {
1226
history: History | null;
1327
onManageChatKey?: () => void;
1428
currentUser?: string;
1529
channel?: Channel;
30+
contact?: DirectContact;
1631
}
1732

1833
const ChatsDropdownMenu = (props: Props) => {
@@ -22,6 +37,24 @@ const ChatsDropdownMenu = (props: Props) => {
2237
const { mutateAsync: leaveChannel, isLoading: isLeavingLoading } = useLeaveCommunityChannel(
2338
props.channel
2439
);
40+
const {
41+
mutateAsync: pinContact,
42+
isLoading: isContactPinning,
43+
isSuccess: isPinned,
44+
isError: isPinFailed
45+
} = usePinContact();
46+
47+
useEffect(() => {
48+
if (isPinned) {
49+
success(_t("g.success"));
50+
}
51+
}, [isPinned]);
52+
53+
useEffect(() => {
54+
if (isPinFailed) {
55+
error(_t("g.error"));
56+
}
57+
}, [isPinFailed]);
2558

2659
const handleExtendedView = () => {
2760
history?.push("/chats");
@@ -34,6 +67,19 @@ const ChatsDropdownMenu = (props: Props) => {
3467
{kebabMenuSvg}
3568
</Button>
3669
<DropdownMenu align="right">
70+
{props.contact && (
71+
<DropdownItemWithIcon
72+
icon={pinSvg}
73+
label={props.contact.pinned ? _t("chat.unpin") : _t("chat.pin")}
74+
disabled={isContactPinning}
75+
onClick={(e: { stopPropagation: () => void }) => {
76+
e.stopPropagation();
77+
if (!isContactPinning) {
78+
pinContact({ contact: props.contact!, pinned: !props.contact!.pinned });
79+
}
80+
}}
81+
/>
82+
)}
3783
{!location.pathname.startsWith("/chats") && (
3884
<DropdownItemWithIcon
3985
icon={extendedView}

src/common/features/chats/components/chats-sidebar/chat-sidebar-direct-contact.tsx

Lines changed: 110 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useContext, useMemo } from "react";
1+
import React, { useContext, useEffect, useMemo, useState } from "react";
22
import UserAvatar from "../../../../components/user-avatar";
33
import { classNameObject } from "../../../../helper/class-name-object";
44
import {
@@ -8,15 +8,19 @@ import {
88
useGetPublicKeysQuery,
99
useKeysQuery,
1010
useLastMessageQuery,
11+
usePinContact,
1112
useUnreadCountQuery
1213
} from "@ecency/ns-query";
1314
import { _t } from "../../../../i18n";
1415
import Tooltip from "../../../../components/tooltip";
15-
import { informationOutlineSvg } from "../../../../img/svg";
16+
import { informationOutlineSvg, pinSvg } from "../../../../img/svg";
1617
import { Button } from "@ui/button";
17-
import { Link } from "react-router-dom";
1818
import { Badge } from "@ui/badge";
1919
import { ChatSidebarSavedMessagesAvatar } from "./chat-sidebar-saved-messages-avatar";
20+
import { Dropdown, DropdownItemWithIcon, DropdownMenu } from "@ui/dropdown";
21+
import { error, success } from "../../../../components/feedback";
22+
import { history } from "../../../../store";
23+
import useDebounce from "react-use/lib/useDebounce";
2024

2125
interface Props {
2226
contact: DirectContact;
@@ -28,6 +32,9 @@ export function ChatSidebarDirectContact({ contact, onClick, isLink = true }: Pr
2832
const { receiverPubKey, setReceiverPubKey, revealPrivateKey, setRevealPrivateKey } =
2933
useContext(ChatContext);
3034

35+
const [holdStarted, setHoldStarted] = useState(false);
36+
const [showContextMenu, setShowContextMenu] = useState(false);
37+
3138
const { publicKey } = useKeysQuery();
3239
const { data: contactKeys, isLoading: isContactKeysLoading } = useGetPublicKeysQuery(
3340
contact.name
@@ -43,6 +50,36 @@ export function ChatSidebarDirectContact({ contact, onClick, isLink = true }: Pr
4350
);
4451
const isActiveUser = useMemo(() => contact.pubkey === publicKey, [publicKey, contact]);
4552

53+
const {
54+
mutateAsync: pinContact,
55+
isLoading: isContactPinning,
56+
isSuccess: isPinned,
57+
isError: isPinFailed
58+
} = usePinContact();
59+
60+
useEffect(() => {
61+
if (isPinned) {
62+
success(_t("g.success"));
63+
}
64+
}, [isPinned]);
65+
66+
useEffect(() => {
67+
if (isPinFailed) {
68+
error(_t("g.error"));
69+
}
70+
}, [isPinFailed]);
71+
72+
useDebounce(
73+
() => {
74+
if (holdStarted) {
75+
setHoldStarted(false);
76+
setShowContextMenu(true);
77+
}
78+
},
79+
500,
80+
[holdStarted]
81+
);
82+
4683
const content = (
4784
<>
4885
<div
@@ -75,8 +112,9 @@ export function ChatSidebarDirectContact({ contact, onClick, isLink = true }: Pr
75112
</Tooltip>
76113
</div>
77114
)}
78-
<div className="font-semibold truncate dark:text-white">
115+
<div className="font-semibold truncate dark:text-white flex items-center gap-2">
79116
{isActiveUser ? _t("chat.saved-messages") : contact.name}
117+
{contact.pinned && <span className="rotate-45 opacity-25 w-3.5">{pinSvg}</span>}
80118
</div>
81119
</div>
82120
<div className="text-xs text-gray-500">{lastMessageDate}</div>
@@ -94,40 +132,74 @@ export function ChatSidebarDirectContact({ contact, onClick, isLink = true }: Pr
94132
</>
95133
);
96134

97-
return isLink ? (
98-
<Link
99-
to="/chats"
100-
className={classNameObject({
101-
"flex items-center text-dark-200 gap-3 p-3 border-b border-[--border-color] last:border-0 hover:bg-gray-100 dark:hover:bg-gray-800 cursor-pointer":
102-
true,
103-
"bg-gray-100 dark:bg-gray-800": receiverPubKey === contact.pubkey
104-
})}
105-
onClick={() => {
106-
setReceiverPubKey(contact.pubkey);
107-
if (revealPrivateKey) {
108-
setRevealPrivateKey(false);
109-
}
110-
onClick?.();
111-
}}
112-
>
113-
{content}
114-
</Link>
115-
) : (
116-
<div
117-
className={classNameObject({
118-
"flex items-center text-dark-200 gap-3 p-3 border-b border-[--border-color] last:border-0 hover:bg-gray-100 dark:hover:bg-gray-800 cursor-pointer":
119-
true,
120-
"bg-gray-100 dark:bg-gray-800": receiverPubKey === contact.pubkey
121-
})}
122-
onClick={() => {
123-
setReceiverPubKey(contact.pubkey);
124-
if (revealPrivateKey) {
125-
setRevealPrivateKey(false);
126-
}
127-
onClick?.();
128-
}}
135+
return (
136+
<Dropdown
137+
closeOnClickOutside={true}
138+
show={showContextMenu}
139+
setShow={(v) => setShowContextMenu(v)}
129140
>
130-
{content}
131-
</div>
141+
<div
142+
onMouseDown={() => setHoldStarted(true)}
143+
onMouseUp={() => {
144+
setHoldStarted(false);
145+
146+
setTimeout(() => {
147+
if (!holdStarted) {
148+
return;
149+
}
150+
151+
if (isLink) {
152+
history?.push("/chats");
153+
}
154+
155+
setReceiverPubKey(contact.pubkey);
156+
if (revealPrivateKey) {
157+
setRevealPrivateKey(false);
158+
}
159+
onClick?.();
160+
}, 500);
161+
}}
162+
onTouchStart={() => setHoldStarted(true)}
163+
onTouchEnd={() => {
164+
setHoldStarted(false);
165+
console.log("touch ended");
166+
167+
setTimeout(() => {
168+
if (!holdStarted) {
169+
return;
170+
}
171+
172+
if (isLink) {
173+
history?.push("/chats");
174+
}
175+
176+
setReceiverPubKey(contact.pubkey);
177+
if (revealPrivateKey) {
178+
setRevealPrivateKey(false);
179+
}
180+
onClick?.();
181+
}, 500);
182+
}}
183+
onContextMenu={(e) => {
184+
e.stopPropagation();
185+
e.preventDefault();
186+
setShowContextMenu(true);
187+
}}
188+
className={classNameObject({
189+
"flex items-center text-dark-200 gap-3 p-3 border-b border-[--border-color] last:border-0 hover:bg-gray-100 dark:hover:bg-gray-800 cursor-pointer":
190+
true,
191+
"bg-gray-100 dark:bg-gray-800": receiverPubKey === contact.pubkey
192+
})}
193+
>
194+
{content}
195+
</div>
196+
<DropdownMenu className="top-[70%]">
197+
<DropdownItemWithIcon
198+
icon={pinSvg}
199+
label={contact.pinned ? _t("chat.unpin") : _t("chat.pin")}
200+
onClick={() => pinContact({ contact, pinned: !contact.pinned })}
201+
/>
202+
</DropdownMenu>
203+
</Dropdown>
132204
);
133205
}

src/common/i18n/locales/en-US.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,9 @@
8888
"retry": "Retry",
8989
"message": "Message",
9090
"actions": "Actions",
91-
"restore": "Restore"
91+
"restore": "Restore",
92+
"success": "Success",
93+
"error": "Error"
9294
},
9395
"confirm": {
9496
"title": "Are you sure?",
@@ -1928,6 +1930,8 @@
19281930
"hidden-messages-management": "Hidden messages management",
19291931
"saved-messages": "Saved messages",
19301932
"fetch-error": "While you were away, network change detected. Reload page now to fetch new data",
1933+
"pin": "Pin",
1934+
"unpin": "Unpin",
19311935
"welcome": {
19321936
"title": "Welcome to Chats",
19331937
"description": "Create or import an account start using Chats",

0 commit comments

Comments
 (0)