Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
402 changes: 402 additions & 0 deletions TASKS.md

Large diffs are not rendered by default.

9 changes: 7 additions & 2 deletions app/src/main/java/org/thunderdog/challegram/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -783,8 +783,9 @@ public void onPasscodeShowing (BaseActivity context, boolean isShowing) {
}
}
long messageId = intent.getLongExtra("message_id", 0);
long messageThreadId = intent.getLongExtra("message_thread_id", 0);
if (accountId != TdlibAccount.NO_ID && chatId != 0) {
openMessagesController(accountId, chatId, messageId);
openMessagesController(accountId, chatId, messageId, messageThreadId);
return true;
} else {
Log.e("Cannot open chat, no information found: %s", intent);
Expand Down Expand Up @@ -1384,7 +1385,7 @@ private void openMainController (int accountId) {
}
}

private void openMessagesController (int accountId, long chatId, long specificMessageId) {
private void openMessagesController (int accountId, long chatId, long specificMessageId, long messageThreadId) {
final Tdlib tdlib = TdlibManager.instanceForAccountId(accountId).account(accountId).tdlib();
tdlib.awaitInitialization(() -> {
tdlib.incrementUiReferenceCount();
Expand All @@ -1393,6 +1394,10 @@ private void openMessagesController (int accountId, long chatId, long specificMe
final TdlibUi.ChatOpenParameters params = new TdlibUi.ChatOpenParameters().onDone(tdlib::decrementUiReferenceCount);
if (specificMessageId != 0)
params.highlightMessage(new MessageId(chatId, specificMessageId));
// Handle forum topic opening
if (messageThreadId != 0) {
params.messageTopic(new TdApi.MessageTopicForum((int) messageThreadId));
}
tdlib.ui().openChat(context, chatId, params);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,30 +15,48 @@
package org.thunderdog.challegram.component.chat;

import android.content.Context;
import android.graphics.Canvas;
import android.view.MotionEvent;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import org.drinkless.tdlib.TdApi;
import org.thunderdog.challegram.data.AvatarPlaceholder;
import org.thunderdog.challegram.data.ThreadInfo;
import org.thunderdog.challegram.loader.AvatarReceiver;
import org.thunderdog.challegram.loader.ComplexReceiver;
import org.thunderdog.challegram.loader.ImageFile;
import org.thunderdog.challegram.loader.gif.GifFile;
import org.thunderdog.challegram.navigation.ComplexHeaderView;
import org.thunderdog.challegram.navigation.HeaderView;
import org.thunderdog.challegram.navigation.ViewController;
import org.thunderdog.challegram.telegram.Tdlib;
import org.thunderdog.challegram.telegram.TdlibAccentColor;
import org.thunderdog.challegram.telegram.TdlibEmojiManager;
import org.thunderdog.challegram.theme.Theme;
import org.thunderdog.challegram.theme.ThemeDeprecated;
import org.thunderdog.challegram.tool.Paints;
import org.thunderdog.challegram.tool.Screen;
import org.thunderdog.challegram.util.text.Letters;

import me.vkryl.core.StringUtils;
import tgx.td.ChatId;

public class ChatHeaderView extends ComplexHeaderView {
public class ChatHeaderView extends ComplexHeaderView implements TdlibEmojiManager.Watcher {
public interface Callback {
void onChatHeaderClick ();
}

private Callback callback;

// Topic icon support
private long topicCustomEmojiId;
private TdlibEmojiManager.Entry topicCustomEmoji;
private ComplexReceiver topicIconReceiver;
private ImageFile topicImageFile;
private GifFile topicGifFile;

public ChatHeaderView (Context context, Tdlib tdlib, @Nullable ViewController<?> parent) {
super(context, tdlib, parent);
setPhotoOpenDisabled(true);
Expand All @@ -50,6 +68,81 @@ public ChatHeaderView (Context context, Tdlib tdlib, @Nullable ViewController<?>
setUseDefaultClickListener(true);
setBackgroundResource(ThemeDeprecated.headerSelector());
setInnerMargins(Screen.dp(56f), Screen.dp(49f));
topicIconReceiver = new ComplexReceiver(this);
}

@Override
protected void onAttachedToWindow () {
super.onAttachedToWindow();
topicIconReceiver.attach();
}

@Override
protected void onDetachedFromWindow () {
super.onDetachedFromWindow();
topicIconReceiver.detach();
}

@Override
public void onCustomEmojiLoaded (TdlibEmojiManager context, TdlibEmojiManager.Entry entry) {
if (entry.customEmojiId == topicCustomEmojiId) {
topicCustomEmoji = entry;
loadTopicEmojiFiles();
invalidate();
}
}

private void loadTopicEmojiFiles () {
if (topicCustomEmoji == null || topicCustomEmoji.isNotFound()) {
topicImageFile = null;
topicGifFile = null;
topicIconReceiver.clear();
return;
}

TdApi.Sticker sticker = topicCustomEmoji.value;
if (sticker == null) {
topicImageFile = null;
topicGifFile = null;
topicIconReceiver.clear();
return;
}

int size = Screen.dp(40f); // Avatar size
// Check sticker format to determine file type
// WebP stickers use ImageFile, TGS and WEBM use GifFile
switch (sticker.format.getConstructor()) {
case TdApi.StickerFormatWebp.CONSTRUCTOR: {
topicImageFile = new ImageFile(tdlib, sticker.sticker);
topicImageFile.setSize(size);
topicImageFile.setScaleType(ImageFile.CENTER_CROP);
topicGifFile = null;
topicIconReceiver.getImageReceiver(0).requestFile(topicImageFile);
break;
}
case TdApi.StickerFormatTgs.CONSTRUCTOR:
case TdApi.StickerFormatWebm.CONSTRUCTOR: {
topicGifFile = new GifFile(tdlib, sticker);
topicGifFile.setOptimizationMode(GifFile.OptimizationMode.EMOJI);
topicGifFile.setRequestedSize(size);
topicImageFile = null;
topicIconReceiver.getGifReceiver(0).requestFile(topicGifFile);
break;
}
}
}

private void clearTopicEmoji () {
if (topicCustomEmojiId != 0 && topicCustomEmoji == null && tdlib != null) {
tdlib.emoji().forgetWatcher(topicCustomEmojiId, this);
}
topicCustomEmojiId = 0;
topicCustomEmoji = null;
topicImageFile = null;
topicGifFile = null;
if (topicIconReceiver != null) {
topicIconReceiver.clear();
}
}

private CharSequence forcedSubtitle;
Expand Down Expand Up @@ -91,20 +184,68 @@ public boolean onTouchEvent (MotionEvent e) {
}

public void setChat (Tdlib tdlib, TdApi.Chat chat, @Nullable ThreadInfo messageThread) {
setChat(tdlib, chat, messageThread, null);
}

public void setChat (Tdlib tdlib, TdApi.Chat chat, @Nullable ThreadInfo messageThread, @Nullable TdApi.ForumTopic forumTopic) {
this.tdlib = tdlib;

// Clear previous topic emoji
clearTopicEmoji();

if (chat == null) {
setText("Debug controller", "nobody should find this view");
return;
}

getAvatarReceiver().requestChat(tdlib, chat.id, AvatarReceiver.Options.FULL_SIZE);
// For forum topics, show topic icon instead of chat avatar
if (forumTopic != null) {
TdApi.ForumTopicIcon icon = forumTopic.info.icon;
int topicColor = icon != null ? icon.color : 0x6FB9F0;
// Ensure color has alpha
if (topicColor < 0x01000000) {
topicColor = 0xFF000000 | topicColor;
}

// Check if topic has custom emoji icon
if (icon != null && icon.customEmojiId != 0) {
// Request custom emoji
topicCustomEmojiId = icon.customEmojiId;
topicCustomEmoji = tdlib.emoji().findOrPostponeRequest(topicCustomEmojiId, this);
if (topicCustomEmoji != null) {
loadTopicEmojiFiles();
}
// Use placeholder while loading or as fallback
String letter = forumTopic.info.forumTopicId == 1 ? "#" :
(!StringUtils.isEmpty(forumTopic.info.name) ? forumTopic.info.name.substring(0, 1).toUpperCase() : "?");
TdlibAccentColor accentColor = new TdlibAccentColor(topicColor);
AvatarPlaceholder.Metadata metadata = new AvatarPlaceholder.Metadata(accentColor, new Letters(letter));
getAvatarReceiver().requestPlaceholder(tdlib, metadata, AvatarReceiver.Options.FULL_SIZE);
} else {
// No custom emoji - show colored placeholder with letter/hash
String letter = forumTopic.info.forumTopicId == 1 ? "#" :
(!StringUtils.isEmpty(forumTopic.info.name) ? forumTopic.info.name.substring(0, 1).toUpperCase() : "?");
TdlibAccentColor accentColor = new TdlibAccentColor(topicColor);
AvatarPlaceholder.Metadata metadata = new AvatarPlaceholder.Metadata(accentColor, new Letters(letter));
getAvatarReceiver().requestPlaceholder(tdlib, metadata, AvatarReceiver.Options.FULL_SIZE);
}
} else {
getAvatarReceiver().requestChat(tdlib, chat.id, AvatarReceiver.Options.FULL_SIZE);
}

setShowVerify(tdlib.chatVerified(chat));
setShowScam(tdlib.chatScam(chat));
setShowFake(tdlib.chatFake(chat));
setShowMute(tdlib.chatNeedsMuteIcon(chat));
setShowLock(ChatId.isSecret(chat.id));
if (messageThread != null) {
if (forumTopic != null) {
// Forum topic: show topic name as title, chat name as subtitle
setEmojiStatus(null);
setText(forumTopic.info.name, !StringUtils.isEmpty(forcedSubtitle) ? forcedSubtitle : tdlib.chatTitle(chat));
setExpandedSubtitle(null);
setUseRedHighlight(false);
attachChatStatus(chat.id, new TdApi.MessageTopicForum(forumTopic.info.forumTopicId));
} else if (messageThread != null) {
setEmojiStatus(null);
setText(messageThread.chatHeaderTitle(), !StringUtils.isEmpty(forcedSubtitle) ? forcedSubtitle : messageThread.chatHeaderSubtitle());
setExpandedSubtitle(null);
Expand All @@ -129,4 +270,32 @@ public void updateUserStatus (TdApi.Chat chat) {
setExpandedSubtitle(tdlib.status().chatStatusExpanded(chat));
}
}

@Override
protected void onDraw (@NonNull Canvas c) {
super.onDraw(c);

// Draw topic custom emoji icon on top of avatar if loaded
if (topicCustomEmojiId != 0 && topicCustomEmoji != null && !topicCustomEmoji.isNotFound()) {
AvatarReceiver avatarReceiver = getAvatarReceiver();
int left = avatarReceiver.getLeft();
int top = avatarReceiver.getTop();
int right = avatarReceiver.getRight();
int bottom = avatarReceiver.getBottom();
float centerX = (left + right) / 2f;
float centerY = (top + bottom) / 2f;
float radius = (right - left) / 2f;

// Cover the placeholder with header background before drawing emoji
c.drawCircle(centerX, centerY, radius, Paints.fillingPaint(Theme.headerColor()));

if (topicImageFile != null) {
topicIconReceiver.getImageReceiver(0).setBounds(left, top, right, bottom);
topicIconReceiver.getImageReceiver(0).draw(c);
} else if (topicGifFile != null) {
topicIconReceiver.getGifReceiver(0).setBounds(left, top, right, bottom);
topicIconReceiver.getGifReceiver(0).draw(c);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1151,6 +1151,11 @@ private void load (final MessageId fromMessageId, final int offset, final int li
loadingLocal = false;
Log.ensureReturnType(TdApi.GetMessageThreadHistory.class, TdApi.Messages.class);
function = new TdApi.GetMessageThreadHistory(sourceChatId, messageThread.getOldestMessageId(), (lastFromMessageId = fromMessageId).getMessageId(), lastOffset = offset, lastLimit = limit);
} else if (topicId != null && topicId.getConstructor() == TdApi.MessageTopicForum.CONSTRUCTOR) {
loadingLocal = false;
Log.ensureReturnType(TdApi.GetForumTopicHistory.class, TdApi.Messages.class);
int forumTopicId = ((TdApi.MessageTopicForum) topicId).forumTopicId;
function = new TdApi.GetForumTopicHistory(sourceChatId, forumTopicId, (lastFromMessageId = fromMessageId).getMessageId(), lastOffset = offset, lastLimit = limit);
} else {
Log.ensureReturnType(TdApi.GetChatHistory.class, TdApi.Messages.class);
function = new TdApi.GetChatHistory(sourceChatId, (lastFromMessageId = fromMessageId).getMessageId(), lastOffset = offset, lastLimit = limit, loadingLocal);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -601,7 +601,7 @@ public void openSearch (TdApi.Chat chat, String query, TdApi.MessageSender sende
loader.setSearchParameters(query, sender, filter);
adapter.setChatType(chat.type);
if (filter != null && Td.isPinnedFilter(filter)) {
initPinned(chat.id, 1, 1);
initPinned(chat.id, null, 1, 1);
}
if (highlightMessageId != null) {
loadFromMessage(highlightMessageId, highlightMode, true);
Expand Down Expand Up @@ -692,8 +692,8 @@ private void setPinnedMessagesAvailable (boolean areAvailable) {
}
}

private void initPinned (long chatId, int initialLoadCount, int loadCount) {
this.pinnedMessages = new MessageListManager(tdlib, initialLoadCount, loadCount, pinnedMessageListener, chatId, 0, null, null, null, new TdApi.SearchMessagesFilterPinned());
private void initPinned (long chatId, @Nullable TdApi.MessageTopic topicId, int initialLoadCount, int loadCount) {
this.pinnedMessages = new MessageListManager(tdlib, initialLoadCount, loadCount, pinnedMessageListener, chatId, 0, topicId, null, null, new TdApi.SearchMessagesFilterPinned());
this.pinnedMessages.addMaxMessageIdListener(pinnedMessageAvailabilityChangeListener);
this.pinnedMessages.addChangeListener(new MessageListManager.ChangeListener() {
@Override
Expand All @@ -718,7 +718,7 @@ public void openChat (TdApi.Chat chat, @Nullable ThreadInfo messageThread, @Null
// readOneShot = true;
}
if (chat.id != 0 && messageThread == null && !areScheduled && needPinnedMessages) {
initPinned(chat.id, 10, 50);
initPinned(chat.id, topicId, 10, 50);
} else {
this.pinnedMessages = null;
}
Expand Down Expand Up @@ -781,6 +781,13 @@ public boolean onMessageViewed (TdlibMessageViewer.Viewport viewport, View view,

@Override
public boolean needForceRead (TdlibMessageViewer.Viewport viewport) {
// For forum topics opened at first unread position, don't force-read on initial load
// to avoid marking all visible messages as read on the server
TdApi.MessageTopic topicId = loader.getTopicId();
if (topicId != null && topicId.getConstructor() == TdApi.MessageTopicForum.CONSTRUCTOR) {
// Only force-read after user has scrolled
return canRead() && wasScrollByUser;
}
return canRead();
}

Expand Down Expand Up @@ -1838,8 +1845,11 @@ private void updateNewMessage (TGMessage message) {
case MessagesLoader.SPECIAL_MODE_SEARCH:
return;
}
TdApi.MessageTopic topicId = loader.getMessageTopicId();
if (!Td.matchesTopic(message.getMessageTopicId(), topicId)) {
// Check both messageThread topic (for comment threads) and forum topicId (for forum tabs)
TdApi.MessageTopic messageThreadTopicId = loader.getMessageTopicId();
TdApi.MessageTopic forumTopicId = loader.getTopicId();
TdApi.MessageTopic effectiveTopicId = messageThreadTopicId != null ? messageThreadTopicId : forumTopicId;
if (!Td.matchesTopic(message.getMessageTopicId(), effectiveTopicId)) {
return;
}
ThreadInfo messageThread = loader.getMessageThread();
Expand Down Expand Up @@ -2551,10 +2561,9 @@ private void saveScrollPosition () {
if (view != null && view.getParent() != null) {
scrollOffsetInPixels = calculateOffsetInPixels(view, message.getExtraPadding());
}
if (readFully && scrollOffsetInPixels == 0) {
scrollMessageId = scrollMessageChatId = 0;
scrollMessageOtherIds = null;
} else if (isBottomSponsored) {
// Don't clear scroll position when at bottom - keep it so we can restore to bottom on re-entry.
// The readFully flag is still saved in SavedMessageId to indicate user was at the end.
if (isBottomSponsored) {
if (message.isSponsoredMessage()) {
// the bottom VISIBLE message is sponsored - no need to save that data
scrollMessageId = scrollMessageChatId = scrollOffsetInPixels = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,14 @@ public int updateChatUnreadMentionCount (long chatId, int unreadMentionCount) {
return -1;
}

public int updateForumUnreadTopicCount (long chatId) {
int index = indexOfChat(chatId);
if (index != -1 && chats.get(index).updateForumUnreadTopicCount(chatId)) {
return getItemPositionByChatIndex(index);
}
return -1;
}

public int updateChatHasScheduledMessages (long chatId, boolean hasScheduledMessages) {
int index = indexOfChat(chatId);
if (index != -1 && chats.get(index).updateChatHasScheduledMessages(chatId, hasScheduledMessages)) {
Expand Down
Loading