feat: Support get archived chats.

This commit is contained in:
jarvis2f 2025-01-21 21:10:37 +08:00
parent ca08df92fd
commit 5d0d2a4943
7 changed files with 96 additions and 19 deletions

View file

@ -380,9 +380,10 @@ public class HttpVerticle extends AbstractVerticle {
} }
String query = ctx.request().getParam("query"); String query = ctx.request().getParam("query");
String chatId = ctx.request().getParam("chatId"); String chatId = ctx.request().getParam("chatId");
String archived = ctx.request().getParam("archived");
getTelegramVerticle(telegramId) getTelegramVerticle(telegramId)
.ifPresentOrElse(telegramVerticle -> .ifPresentOrElse(telegramVerticle ->
telegramVerticle.getChats(Convert.toLong(chatId), query) telegramVerticle.getChats(Convert.toLong(chatId), query, Convert.toBool(archived, false))
.onSuccess(ctx::json) .onSuccess(ctx::json)
.onFailure(ctx::fail), () -> ctx.fail(404)); .onFailure(ctx::fail), () -> ctx.fail(404));
} }

View file

@ -4,8 +4,12 @@ import cn.hutool.core.util.StrUtil;
import cn.hutool.log.Log; import cn.hutool.log.Log;
import cn.hutool.log.LogFactory; import cn.hutool.log.LogFactory;
import org.drinkless.tdlib.TdApi; import org.drinkless.tdlib.TdApi;
import org.jooq.lambda.tuple.Tuple;
import java.util.*; import java.util.List;
import java.util.NavigableSet;
import java.util.Objects;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -19,14 +23,18 @@ public class TelegramChats {
private final NavigableSet<OrderedChat> mainChatList = new TreeSet<>(); private final NavigableSet<OrderedChat> mainChatList = new TreeSet<>();
private final NavigableSet<OrderedChat> archivedChatList = new TreeSet<>();
private boolean haveFullMainChatList = false; private boolean haveFullMainChatList = false;
private boolean haveFullArchivedChatList = false;
public TelegramChats(TelegramClient client) { public TelegramChats(TelegramClient client) {
this.client = client; this.client = client;
} }
public List<TdApi.Chat> getMainChatList(Long activatedChatId, String query, int limit) { public List<TdApi.Chat> getChatList(Long activatedChatId, String query, int limit, boolean archived) {
List<TdApi.Chat> chatList = mainChatList.stream() List<TdApi.Chat> chatList = (archived ? archivedChatList : mainChatList).stream()
.map(OrderedChat::chatId) .map(OrderedChat::chatId)
.map(chats::get) .map(chats::get)
.filter(Objects::nonNull) .filter(Objects::nonNull)
@ -65,6 +73,27 @@ public class TelegramChats {
} }
} }
public void loadArchivedChatList() {
synchronized (archivedChatList) {
if (!haveFullArchivedChatList) {
// send LoadChats request if there are some unknown chats and have not enough known chats
client.execute(new TdApi.LoadChats(new TdApi.ChatListArchive(), 100))
.onSuccess(object -> {
// chats had already been received through updates, let's retry request
loadArchivedChatList();
})
.onFailure(error -> {
if (((TelegramRunException) error).getError().code == 404) {
synchronized (archivedChatList) {
haveFullArchivedChatList = true;
log.debug("Archived chat list is loaded, size: %d".formatted(archivedChatList.size()));
}
}
});
}
}
}
public void onChatUpdated(TdApi.Object object) { public void onChatUpdated(TdApi.Object object) {
switch (object.getConstructor()) { switch (object.getConstructor()) {
case TdApi.UpdateNewChat.CONSTRUCTOR: { case TdApi.UpdateNewChat.CONSTRUCTOR: {
@ -74,7 +103,6 @@ public class TelegramChats {
chats.put(chat.id, chat); chats.put(chat.id, chat);
TdApi.ChatPosition[] positions = chat.positions; TdApi.ChatPosition[] positions = chat.positions;
chat.positions = new TdApi.ChatPosition[0];
setChatPositions(chat, positions); setChatPositions(chat, positions);
} }
break; break;
@ -115,7 +143,8 @@ public class TelegramChats {
} }
case TdApi.UpdateChatPosition.CONSTRUCTOR: { case TdApi.UpdateChatPosition.CONSTRUCTOR: {
TdApi.UpdateChatPosition updateChat = (TdApi.UpdateChatPosition) object; TdApi.UpdateChatPosition updateChat = (TdApi.UpdateChatPosition) object;
if (updateChat.position.list.getConstructor() != TdApi.ChatListMain.CONSTRUCTOR) { if (updateChat.position.list.getConstructor() != TdApi.ChatListMain.CONSTRUCTOR
&& updateChat.position.list.getConstructor() != TdApi.ChatListArchive.CONSTRUCTOR) {
break; break;
} }
@ -123,7 +152,7 @@ public class TelegramChats {
synchronized (chat) { synchronized (chat) {
int i; int i;
for (i = 0; i < chat.positions.length; i++) { for (i = 0; i < chat.positions.length; i++) {
if (chat.positions[i].list.getConstructor() == TdApi.ChatListMain.CONSTRUCTOR) { if (chat.positions[i].list.getConstructor() == updateChat.position.list.getConstructor()) {
break; break;
} }
} }
@ -147,7 +176,7 @@ public class TelegramChats {
} }
private void setChatPositions(TdApi.Chat chat, TdApi.ChatPosition[] positions) { private void setChatPositions(TdApi.Chat chat, TdApi.ChatPosition[] positions) {
synchronized (mainChatList) { synchronized (Tuple.tuple(mainChatList, archivedChatList)) {
synchronized (chat) { synchronized (chat) {
for (TdApi.ChatPosition position : chat.positions) { for (TdApi.ChatPosition position : chat.positions) {
if (position.list.getConstructor() == TdApi.ChatListMain.CONSTRUCTOR) { if (position.list.getConstructor() == TdApi.ChatListMain.CONSTRUCTOR) {
@ -155,6 +184,11 @@ public class TelegramChats {
log.warn("Chat %d was not found in mainChatList".formatted(chat.id)); log.warn("Chat %d was not found in mainChatList".formatted(chat.id));
} }
} }
if (position.list.getConstructor() == TdApi.ChatListArchive.CONSTRUCTOR) {
if (!archivedChatList.remove(new OrderedChat(chat.id, position))) {
log.warn("Chat %d was not found in archivedChatList".formatted(chat.id));
}
}
} }
chat.positions = positions; chat.positions = positions;
@ -165,6 +199,11 @@ public class TelegramChats {
log.warn("Chat %d was already in mainChatList".formatted(chat.id)); log.warn("Chat %d was already in mainChatList".formatted(chat.id));
} }
} }
if (position.list.getConstructor() == TdApi.ChatListArchive.CONSTRUCTOR) {
if (!archivedChatList.add(new OrderedChat(chat.id, position))) {
log.warn("Chat %d was already in archivedChatList".formatted(chat.id));
}
}
} }
} }
} }

View file

@ -171,8 +171,8 @@ public class TelegramVerticle extends AbstractVerticle {
}); });
} }
public Future<JsonArray> getChats(Long activatedChatId, String query) { public Future<JsonArray> getChats(Long activatedChatId, String query, boolean archived) {
return this.convertChat(telegramChats.getMainChatList(activatedChatId, query, 100)); return this.convertChat(telegramChats.getChatList(activatedChatId, query, 100, archived));
} }
public Future<JsonObject> getChatFiles(long chatId, MultiMap filter) { public Future<JsonObject> getChatFiles(long chatId, MultiMap filter) {
@ -753,6 +753,7 @@ public class TelegramVerticle extends AbstractVerticle {
} }
sendHttpEvent(EventPayload.build(EventPayload.TYPE_AUTHORIZATION, authorizationState)); sendHttpEvent(EventPayload.build(EventPayload.TYPE_AUTHORIZATION, authorizationState));
telegramChats.loadMainChatList(); telegramChats.loadMainChatList();
telegramChats.loadArchivedChatList();
break; break;
case TdApi.AuthorizationStateLoggingOut.CONSTRUCTOR: case TdApi.AuthorizationStateLoggingOut.CONSTRUCTOR:
break; break;

View file

@ -1,4 +1,4 @@
import { Check, ChevronsUpDown, Ellipsis } from "lucide-react"; import { Archive, Check, ChevronsUpDown, Ellipsis, List } from "lucide-react";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
import { import {
@ -14,22 +14,30 @@ import { useTelegramChat } from "@/hooks/use-telegram-chat";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { CommandLoading } from "cmdk"; import { CommandLoading } from "cmdk";
import { Toggle } from "./ui/toggle";
import { TooltipWrapper } from "@/components/ui/tooltip";
export default function ChatSelect({ disabled }: { disabled: boolean }) { export default function ChatSelect({ disabled }: { disabled: boolean }) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [archived, setArchived] = useState(false);
const { const {
isLoading, isLoading,
handleQueryChange, handleQueryChange,
chats, chats,
chat: selectedChat, chat: selectedChat,
handleChatChange, handleChatChange,
handleArchivedChange,
} = useTelegramChat(); } = useTelegramChat();
useEffect(() => { useEffect(() => {
handleQueryChange(search); handleQueryChange(search);
}, [search, handleQueryChange]); }, [search, handleQueryChange]);
useEffect(() => {
handleArchivedChange(archived);
}, [archived, handleArchivedChange]);
return ( return (
<Popover open={open} onOpenChange={setOpen}> <Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild disabled={disabled}> <PopoverTrigger asChild disabled={disabled}>
@ -64,12 +72,31 @@ export default function ChatSelect({ disabled }: { disabled: boolean }) {
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-[300px] p-0"> <PopoverContent className="w-[300px] p-0">
<Command shouldFilter={false}> <Command shouldFilter={false}>
<CommandInput <div className="flex w-full border-b">
placeholder="Search chat..." <TooltipWrapper
className="h-9" content={archived ? "Show main chats" : "Show archived chats"}
value={search} >
onValueChange={setSearch} <Toggle
/> className="h-9 rounded-none border-r"
pressed={archived}
onPressedChange={setArchived}
>
{archived ? (
<Archive className="h-5 w-5" />
) : (
<List className="h-5 w-5" />
)}
</Toggle>
</TooltipWrapper>
<div className="flex-1">
<CommandInput
placeholder="Search chat..."
className="h-9"
value={search}
onValueChange={setSearch}
/>
</div>
</div>
<CommandList className="relative"> <CommandList className="relative">
{isLoading && ( {isLoading && (
<CommandLoading> <CommandLoading>

View file

@ -28,6 +28,10 @@ export const SWRProvider = ({ children }: { children: React.ReactNode }) => {
message = err.message; message = err.message;
} }
if (key.startsWith("http")) {
key = new URL(key).pathname;
}
toast({ toast({
description: ( description: (
<div className="w-full flex flex-col space-y-2 overflow-hidden"> <div className="w-full flex flex-col space-y-2 overflow-hidden">

View file

@ -39,7 +39,7 @@ const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>, React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input> React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper=""> <div className="flex items-center px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" /> <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input <CommandPrimitive.Input
ref={ref} ref={ref}

View file

@ -14,8 +14,10 @@ interface TelegramChatContextType {
chat?: TelegramChat; chat?: TelegramChat;
chats: TelegramChat[]; chats: TelegramChat[];
query: string; query: string;
archived: boolean;
handleChatChange: (chatId: string) => void; handleChatChange: (chatId: string) => void;
handleQueryChange: (search: string) => void; handleQueryChange: (search: string) => void;
handleArchivedChange: (archived: boolean) => void;
} }
const TelegramChatContext = createContext<TelegramChatContextType | undefined>( const TelegramChatContext = createContext<TelegramChatContextType | undefined>(
@ -30,6 +32,7 @@ export const TelegramChatProvider: React.FC<TelegramChatProviderProps> = ({
children, children,
}) => { }) => {
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
const [archived, setArchived] = useState(false);
const params = useParams<{ accountId: string; chatId: string }>(); const params = useParams<{ accountId: string; chatId: string }>();
const router = useRouter(); const router = useRouter();
const { toast } = useToast(); const { toast } = useToast();
@ -44,7 +47,7 @@ export const TelegramChatProvider: React.FC<TelegramChatProviderProps> = ({
isLoading, isLoading,
mutate, mutate,
} = useSWR<TelegramChat[]>( } = useSWR<TelegramChat[]>(
`/telegram/${accountId}/chats?query=${query}&chatId=${chatId ?? ""}`, `/telegram/${accountId}/chats?query=${query}&archived=${archived}&chatId=${chatId ?? ""}`,
); );
const chat = useMemo( const chat = useMemo(
@ -77,8 +80,10 @@ export const TelegramChatProvider: React.FC<TelegramChatProviderProps> = ({
chat, chat,
chats: chats ?? [], chats: chats ?? [],
query, query,
archived,
handleChatChange, handleChatChange,
handleQueryChange: handleQueryChange, handleQueryChange: handleQueryChange,
handleArchivedChange: setArchived,
}} }}
> >
{children} {children}