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 chatId = ctx.request().getParam("chatId");
String archived = ctx.request().getParam("archived");
getTelegramVerticle(telegramId)
.ifPresentOrElse(telegramVerticle ->
telegramVerticle.getChats(Convert.toLong(chatId), query)
telegramVerticle.getChats(Convert.toLong(chatId), query, Convert.toBool(archived, false))
.onSuccess(ctx::json)
.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.LogFactory;
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.ConcurrentMap;
import java.util.stream.Collectors;
@ -19,14 +23,18 @@ public class TelegramChats {
private final NavigableSet<OrderedChat> mainChatList = new TreeSet<>();
private final NavigableSet<OrderedChat> archivedChatList = new TreeSet<>();
private boolean haveFullMainChatList = false;
private boolean haveFullArchivedChatList = false;
public TelegramChats(TelegramClient client) {
this.client = client;
}
public List<TdApi.Chat> getMainChatList(Long activatedChatId, String query, int limit) {
List<TdApi.Chat> chatList = mainChatList.stream()
public List<TdApi.Chat> getChatList(Long activatedChatId, String query, int limit, boolean archived) {
List<TdApi.Chat> chatList = (archived ? archivedChatList : mainChatList).stream()
.map(OrderedChat::chatId)
.map(chats::get)
.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) {
switch (object.getConstructor()) {
case TdApi.UpdateNewChat.CONSTRUCTOR: {
@ -74,7 +103,6 @@ public class TelegramChats {
chats.put(chat.id, chat);
TdApi.ChatPosition[] positions = chat.positions;
chat.positions = new TdApi.ChatPosition[0];
setChatPositions(chat, positions);
}
break;
@ -115,7 +143,8 @@ public class TelegramChats {
}
case TdApi.UpdateChatPosition.CONSTRUCTOR: {
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;
}
@ -123,7 +152,7 @@ public class TelegramChats {
synchronized (chat) {
int 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;
}
}
@ -147,7 +176,7 @@ public class TelegramChats {
}
private void setChatPositions(TdApi.Chat chat, TdApi.ChatPosition[] positions) {
synchronized (mainChatList) {
synchronized (Tuple.tuple(mainChatList, archivedChatList)) {
synchronized (chat) {
for (TdApi.ChatPosition position : chat.positions) {
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));
}
}
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;
@ -165,6 +199,11 @@ public class TelegramChats {
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) {
return this.convertChat(telegramChats.getMainChatList(activatedChatId, query, 100));
public Future<JsonArray> getChats(Long activatedChatId, String query, boolean archived) {
return this.convertChat(telegramChats.getChatList(activatedChatId, query, 100, archived));
}
public Future<JsonObject> getChatFiles(long chatId, MultiMap filter) {
@ -753,6 +753,7 @@ public class TelegramVerticle extends AbstractVerticle {
}
sendHttpEvent(EventPayload.build(EventPayload.TYPE_AUTHORIZATION, authorizationState));
telegramChats.loadMainChatList();
telegramChats.loadArchivedChatList();
break;
case TdApi.AuthorizationStateLoggingOut.CONSTRUCTOR:
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 { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
import {
@ -14,22 +14,30 @@ import { useTelegramChat } from "@/hooks/use-telegram-chat";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { cn } from "@/lib/utils";
import { CommandLoading } from "cmdk";
import { Toggle } from "./ui/toggle";
import { TooltipWrapper } from "@/components/ui/tooltip";
export default function ChatSelect({ disabled }: { disabled: boolean }) {
const [open, setOpen] = useState(false);
const [search, setSearch] = useState("");
const [archived, setArchived] = useState(false);
const {
isLoading,
handleQueryChange,
chats,
chat: selectedChat,
handleChatChange,
handleArchivedChange,
} = useTelegramChat();
useEffect(() => {
handleQueryChange(search);
}, [search, handleQueryChange]);
useEffect(() => {
handleArchivedChange(archived);
}, [archived, handleArchivedChange]);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild disabled={disabled}>
@ -64,12 +72,31 @@ export default function ChatSelect({ disabled }: { disabled: boolean }) {
</PopoverTrigger>
<PopoverContent className="w-[300px] p-0">
<Command shouldFilter={false}>
<CommandInput
placeholder="Search chat..."
className="h-9"
value={search}
onValueChange={setSearch}
/>
<div className="flex w-full border-b">
<TooltipWrapper
content={archived ? "Show main chats" : "Show archived chats"}
>
<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">
{isLoading && (
<CommandLoading>

View file

@ -28,6 +28,10 @@ export const SWRProvider = ({ children }: { children: React.ReactNode }) => {
message = err.message;
}
if (key.startsWith("http")) {
key = new URL(key).pathname;
}
toast({
description: (
<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.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ 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" />
<CommandPrimitive.Input
ref={ref}

View file

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