generated from exteraGramPlugins/pluginBase
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2e4075a03 |
No files matched your search
@@ -11,6 +11,9 @@ Shows what a [telegram-panel](../../telegram-panel) message logger saved, native
|
||||
for the history topic in the activity group. Versions and their media are read from the activity group through
|
||||
a signed-in account that can (the current one, the master, or any member); otherwise the
|
||||
server's text is shown and media are marked unavailable;
|
||||
- the comments button of a deleted channel post (whose thread Telegram deletes with it) opens
|
||||
the app's own comments section for it — avatars, names, replies, media — filled from the
|
||||
logged comments (the thread's history request is answered locally);
|
||||
- message links into a chat the current account isn't in (e.g. a log post's "Message" link)
|
||||
open that chat as a signed-in account that is in it; going back returns to your account.
|
||||
Every signed-in account's chats are cached client-side for this.
|
||||
|
||||
+4
-2
@@ -3,9 +3,11 @@
|
||||
"name": "Message Logger",
|
||||
"description": "Shows deleted and edited messages and their edit history from your telegram-panel message logger.",
|
||||
"author": "@lyuksovannyy",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"icon": "exteraPlugins/1",
|
||||
"app_version": ">=12.5.1",
|
||||
"sdk_version": ">=1.4.4.3",
|
||||
"requirements": ["requests"]
|
||||
"requirements": [
|
||||
"requests"
|
||||
]
|
||||
}
|
||||
+13
@@ -54,6 +54,12 @@ class Api:
|
||||
except requests.RequestException as exc:
|
||||
raise ApiError(f"Server unreachable: {exc}") from exc
|
||||
if response.status_code == 404:
|
||||
try:
|
||||
detail = response.json().get("detail")
|
||||
except Exception:
|
||||
detail = None
|
||||
if detail == "Not Found": # the route itself: an older server
|
||||
raise ApiError("the server doesn't support this yet (update telegram-panel)")
|
||||
return None
|
||||
if response.status_code >= 400:
|
||||
try:
|
||||
@@ -127,6 +133,13 @@ class Api:
|
||||
params={"account_id": account_id},
|
||||
)
|
||||
|
||||
def comments(self, account_id, chat_id, message_id):
|
||||
return self._first(
|
||||
"GET",
|
||||
f"/messages/{chat_id}/{message_id}/comments",
|
||||
params={"account_id": account_id},
|
||||
)
|
||||
|
||||
def deletions(self, account_id, chat_id, message_ids):
|
||||
ids = sorted(set(int(i) for i in message_ids))
|
||||
marked = 0
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
"""The comments button of a deleted channel post opens its logged comments
|
||||
(views/comments.py) instead of the app's discussion, which Telegram deleted
|
||||
with the post ("message doesn't exist").
|
||||
|
||||
ChatActivity$ChatMessageCellDelegate.didPressCommentButton(ChatMessageCell)
|
||||
handles the button; an album's comments hang on any of its messages.
|
||||
ConnectionsManager.sendRequestInternal (where every sendRequest overload
|
||||
ends) is where the opened thread's getReplies is answered locally, and
|
||||
ChatActivity.updateBottomOverlay where its input bar is kept hidden.
|
||||
"""
|
||||
|
||||
from base_plugin import MethodHook
|
||||
|
||||
import diag
|
||||
import store
|
||||
from jutil import field, hook_methods, in_background, java_list
|
||||
from views import comments as comments_view
|
||||
|
||||
DELEGATE = "org.telegram.ui.ChatActivity$ChatMessageCellDelegate"
|
||||
CONNECTIONS = "org.telegram.tgnet.ConnectionsManager"
|
||||
|
||||
|
||||
def _deleted_post(cell):
|
||||
"""The deleted message of the cell (or its album), else None."""
|
||||
message = cell.getMessageObject()
|
||||
if message is None:
|
||||
return None
|
||||
candidates = [message]
|
||||
group = cell.getCurrentMessagesGroup()
|
||||
if group is not None:
|
||||
candidates += java_list(group.messages)
|
||||
for candidate in candidates:
|
||||
if candidate is not None and int(candidate.getId()) > 0 and store.is_deleted(
|
||||
int(candidate.currentAccount),
|
||||
int(candidate.getDialogId()),
|
||||
int(candidate.getId()),
|
||||
):
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
class CommentsButton:
|
||||
def __init__(self, plugin, open_comments):
|
||||
self.plugin = plugin
|
||||
self.open_comments = open_comments
|
||||
self.handles = []
|
||||
|
||||
def install(self):
|
||||
button = self
|
||||
|
||||
class BeforePress(MethodHook):
|
||||
def before_hooked_method(self, param):
|
||||
try:
|
||||
post = _deleted_post(param.args[0])
|
||||
if post is not None:
|
||||
param.setResult(None)
|
||||
button.open_comments(post)
|
||||
except Exception as exc:
|
||||
diag.error("comments.press", exc)
|
||||
|
||||
self.handles += hook_methods(
|
||||
self.plugin, DELEGATE, "didPressCommentButton", BeforePress()
|
||||
)
|
||||
|
||||
class BeforeSend(MethodHook):
|
||||
def before_hooked_method(self, param):
|
||||
try:
|
||||
button.answer_locally(param)
|
||||
except Exception as exc:
|
||||
diag.error("comments.request", exc)
|
||||
|
||||
self.handles += hook_methods(
|
||||
self.plugin, CONNECTIONS, "sendRequestInternal", BeforeSend()
|
||||
)
|
||||
|
||||
class AfterOverlay(MethodHook):
|
||||
def after_hooked_method(self, param):
|
||||
try:
|
||||
chat = param.thisObject
|
||||
if int(chat.getClassGuid()) in comments_view.read_only:
|
||||
comments_view.make_read_only(chat)
|
||||
except Exception as exc:
|
||||
diag.error("comments.overlay", exc)
|
||||
|
||||
self.handles += hook_methods(
|
||||
self.plugin, "org.telegram.ui.ChatActivity", "updateBottomOverlay", AfterOverlay()
|
||||
)
|
||||
|
||||
def answer_locally(self, param):
|
||||
"""A thread opened by views/comments.py asks for its replies: answer
|
||||
from the logged comments instead of Telegram (the thread is gone)."""
|
||||
request = param.args[0]
|
||||
if request is None or type(request).__name__ != "TLRPC$TL_messages_getReplies":
|
||||
return
|
||||
account = int(field(param.thisObject, "currentAccount") or 0)
|
||||
response = comments_view.answer(account, request)
|
||||
if response is None:
|
||||
return
|
||||
delegate, timestamped = param.args[1], param.args[2]
|
||||
if param.method.getReturnType().getName() == "void":
|
||||
param.setResult(None)
|
||||
else:
|
||||
from java import jint
|
||||
|
||||
param.setResult(jint(0))
|
||||
|
||||
def deliver():
|
||||
if delegate is not None:
|
||||
delegate.run(response, None)
|
||||
elif timestamped is not None:
|
||||
timestamped.run(response, None, 0)
|
||||
|
||||
in_background(deliver)
|
||||
|
||||
def uninstall(self):
|
||||
for handle in self.handles:
|
||||
try:
|
||||
self.plugin.unhook_method(handle)
|
||||
except Exception:
|
||||
pass
|
||||
self.handles = []
|
||||
+6
-2
@@ -17,11 +17,13 @@ from config import (
|
||||
SERVER_URL,
|
||||
)
|
||||
from hooks.chat import EDIT_UPDATES, ChatSync
|
||||
from hooks.comments import CommentsButton
|
||||
from hooks.deletes import UPDATES, Deletes
|
||||
from hooks.links import Links
|
||||
from hooks.menu import Menu
|
||||
from hooks.render import Render
|
||||
from jutil import in_background, on_ui
|
||||
from views.comments import Comments
|
||||
from views.history import History
|
||||
|
||||
|
||||
@@ -43,7 +45,9 @@ class MessageLoggerPlugin(BasePlugin):
|
||||
self.deletes = Deletes(self, self.api, self.chats)
|
||||
self.links = Links(self)
|
||||
self.menu = Menu(self, self.history.open)
|
||||
for part in (self.render, self.chats, self.deletes, self.links, self.menu):
|
||||
self.comments = CommentsButton(self, Comments(self, self.api).open)
|
||||
parts = (self.render, self.chats, self.deletes, self.links, self.menu, self.comments)
|
||||
for part in parts:
|
||||
try:
|
||||
part.install()
|
||||
except Exception as exc:
|
||||
@@ -62,7 +66,7 @@ class MessageLoggerPlugin(BasePlugin):
|
||||
store.purge_old(CACHE_DAYS_CHOICES[index][1], force=force)
|
||||
|
||||
def on_plugin_unload(self):
|
||||
for part in ("render", "chats", "deletes", "links", "menu"):
|
||||
for part in ("render", "chats", "deletes", "links", "menu", "comments"):
|
||||
if hasattr(self, part):
|
||||
getattr(self, part).uninstall()
|
||||
diag.log("unloaded")
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
"""Comments of a deleted channel post, shown as the app's own comments
|
||||
section: the discussion group's ChatActivity in thread mode, with names,
|
||||
avatars, replies and media as for a post that still exists.
|
||||
|
||||
Telegram deletes a post's discussion thread with it (the comments come back
|
||||
empty, getDiscussionMessage fails with MSG_ID_INVALID), so the comments come
|
||||
from the logger: copies in the activity group's comments topic (each starting
|
||||
with a header, stripped here), read through an account that can (else the
|
||||
server's text, media marked unavailable).
|
||||
|
||||
The thread is opened like ChatActivity.openDiscussionMessageChat does
|
||||
(setThreadMessages with the post as its start message); the history request
|
||||
it then sends (messages.getReplies of the thread) is answered locally from
|
||||
the logged comments (`answer`, hooked in ConnectionsManager by
|
||||
hooks/comments.py), so the app builds the section itself.
|
||||
"""
|
||||
|
||||
import threading
|
||||
|
||||
import accounts
|
||||
import diag
|
||||
import header
|
||||
import peers
|
||||
import tg
|
||||
from api import ApiError
|
||||
from jutil import in_background, java_list, on_ui
|
||||
from views.history import History, _bulletin, _epoch
|
||||
|
||||
# thread id -> (account, discussion channel id, [TL messages], [users], [chats])
|
||||
_threads = {}
|
||||
# classGuids of the comment screens opened here: read-only (no input bar)
|
||||
read_only = set()
|
||||
_lock = threading.Lock()
|
||||
|
||||
|
||||
def make_read_only(chat):
|
||||
"""Swaps a comment screen's input bar for a read-only notice (the app's
|
||||
own bottom overlay, as in chats one can't write to). UI thread."""
|
||||
from android.view import View
|
||||
|
||||
from jutil import field
|
||||
|
||||
enter = field(chat, "chatActivityEnterView")
|
||||
if enter is not None:
|
||||
enter.setVisibility(View.GONE)
|
||||
overlay = field(chat, "bottomOverlay")
|
||||
text = field(chat, "bottomOverlayText")
|
||||
if overlay is None or text is None:
|
||||
return
|
||||
for name in ("bottomOverlayChatText", "bottomOverlayStartButton", "bottomOverlayProgress"):
|
||||
other = field(chat, name)
|
||||
if other is not None:
|
||||
other.setVisibility(View.GONE)
|
||||
# The app animates the overlay out when the thread has an input bar:
|
||||
# stop that, or it ends invisible (alpha 0) after this.
|
||||
animation = field(chat, "bottomOverlayAnimation")
|
||||
if animation is not None:
|
||||
animation.cancel()
|
||||
text.setText("Viewing deleted comments")
|
||||
text.setVisibility(View.VISIBLE)
|
||||
overlay.setAlpha(1.0)
|
||||
overlay.setVisibility(View.VISIBLE)
|
||||
|
||||
|
||||
def _peer(controller, marked):
|
||||
"""TLRPC peer of a Telethon sender id."""
|
||||
from hooks.chat import _android_peer_id
|
||||
|
||||
return controller.getPeer(_android_peer_id(int(marked)))
|
||||
|
||||
|
||||
def _sender_object(controller, marked, name):
|
||||
"""The sender's User/Chat from the account's cache, else a stand-in with
|
||||
the logged name (avatar drawn from its initials)."""
|
||||
from hooks.chat import _android_peer_id
|
||||
from org.telegram.tgnet import TLRPC
|
||||
|
||||
peer_id = _android_peer_id(int(marked))
|
||||
if peer_id > 0:
|
||||
user = controller.getUser(peer_id)
|
||||
if user is None:
|
||||
user = TLRPC.TL_user()
|
||||
user.id = peer_id
|
||||
user.first_name = name or "Deleted"
|
||||
user.flags |= 2
|
||||
return user, None
|
||||
chat = controller.getChat(-peer_id)
|
||||
if chat is None:
|
||||
chat = TLRPC.TL_channel()
|
||||
chat.id = -peer_id
|
||||
chat.title = name or "Channel"
|
||||
chat.broadcast = True
|
||||
return None, chat
|
||||
|
||||
|
||||
class Comments(History):
|
||||
def open(self, message):
|
||||
"""Shows the logged comments of a channel post (MessageObject)."""
|
||||
if message is None:
|
||||
return
|
||||
if not self.api.configured:
|
||||
_bulletin("Message Logger: set the server URL and API token first", True)
|
||||
return
|
||||
account = int(message.currentAccount)
|
||||
chat_id = peers.marked_chat_id(account, int(message.getDialogId()))
|
||||
replies = message.messageOwner.replies
|
||||
if chat_id is None or int(message.getId()) <= 0 or replies is None:
|
||||
_bulletin("No comments logged for this post")
|
||||
return
|
||||
discussion = int(replies.channel_id)
|
||||
in_background(lambda: self._load(message, account, chat_id, discussion))
|
||||
|
||||
def _load(self, original, account, chat_id, discussion):
|
||||
try:
|
||||
result = self.api.comments(
|
||||
peers.own_user_id(account), chat_id, int(original.getId())
|
||||
)
|
||||
except ApiError as exc:
|
||||
_bulletin(f"Message Logger: {exc}", True)
|
||||
return
|
||||
items = (result or {}).get("comments") or []
|
||||
if not items:
|
||||
_bulletin("The post was deleted; no comments were logged for it")
|
||||
return
|
||||
ids = [int(c["message_id"]) for c in items if c.get("message_id")]
|
||||
reader = None
|
||||
if result.get("chat_id") and ids:
|
||||
channel_id = peers.channel_id_from_marked(result["chat_id"])
|
||||
readers = accounts.with_access(
|
||||
-channel_id,
|
||||
prefer=(account, accounts.slot_of(result.get("master_tg_id"))),
|
||||
)
|
||||
reader = readers[0] if readers else None
|
||||
if reader is None:
|
||||
on_ui(lambda: self._open(original, items, None, {}, discussion))
|
||||
return
|
||||
|
||||
def done(found, error):
|
||||
if found is None:
|
||||
diag.log(f"comments: activity group unreadable: {error}")
|
||||
found = {}
|
||||
on_ui(lambda: self._open(original, items, reader, found, discussion))
|
||||
|
||||
tg.get_messages(reader, -channel_id, ids, done)
|
||||
|
||||
# the thread
|
||||
|
||||
def _open(self, original, items, reader, found, discussion):
|
||||
from java import jint
|
||||
from org.telegram.messenger import MessageObject, MessagesController
|
||||
from org.telegram.ui import ChatActivity
|
||||
from android.os import Bundle
|
||||
from client_utils import get_last_fragment
|
||||
|
||||
from views import open_as
|
||||
|
||||
# Drawn as the account that read the copies (their media and the
|
||||
# senders it saw) when it knows the discussion group, else as the
|
||||
# viewer.
|
||||
viewer = int(original.currentAccount)
|
||||
account = viewer
|
||||
if reader is not None and MessagesController.getInstance(reader).getChat(
|
||||
discussion
|
||||
) is not None:
|
||||
account = reader
|
||||
controller = MessagesController.getInstance(account)
|
||||
group = controller.getChat(discussion)
|
||||
if group is None:
|
||||
_bulletin("The post's discussion group isn't known to this account", True)
|
||||
return
|
||||
sources = {int(c["source_message_id"]) for c in items}
|
||||
# Comments answering the post itself reply to its (deleted) copy in
|
||||
# the discussion group: that id is the thread's.
|
||||
tops = [
|
||||
int(c["reply_to"])
|
||||
for c in items
|
||||
if c.get("reply_to") and int(c["reply_to"]) not in sources
|
||||
]
|
||||
top = max(set(tops), key=tops.count) if tops else min(sources) - 1
|
||||
|
||||
messages, users, chats = [], {}, {group.id: group}
|
||||
channel = controller.getChat(-int(original.getDialogId()))
|
||||
if channel is not None:
|
||||
chats[channel.id] = channel
|
||||
for item in items:
|
||||
copy = found.get(int(item.get("message_id") or 0)) if reader is not None else None
|
||||
msg, _drawn_as = self._message_for(original, item, copy, reader)
|
||||
self._into_thread(msg, item, discussion, top, sources)
|
||||
if item.get("sender_id"):
|
||||
user, chat = _sender_object(
|
||||
controller, item["sender_id"], item.get("sender_name")
|
||||
)
|
||||
if user is not None:
|
||||
users[user.id] = user
|
||||
if chat is not None:
|
||||
chats[chat.id] = chat
|
||||
messages.append(msg)
|
||||
with _lock:
|
||||
_threads[top] = (account, discussion, messages, list(users.values()), list(chats.values()))
|
||||
|
||||
start = MessageObject(account, self._start_message(original, discussion, top, len(items)), False, False)
|
||||
args = Bundle()
|
||||
args.putLong("chat_id", discussion)
|
||||
thread = ChatActivity(args)
|
||||
thread.setCurrentAccount(account)
|
||||
from java.util import ArrayList
|
||||
|
||||
starts = ArrayList()
|
||||
starts.add(start)
|
||||
thread.setThreadMessages(starts, group, jint(top), jint(0), jint(0), None)
|
||||
fragment = get_last_fragment()
|
||||
if fragment is None:
|
||||
return
|
||||
read_only.add(int(thread.getClassGuid()))
|
||||
fragment.presentFragment(thread)
|
||||
make_read_only(thread)
|
||||
# Again once the screen settled (its first layout re-runs the
|
||||
# overlay animation).
|
||||
on_ui(lambda: make_read_only(thread), 600)
|
||||
open_as._opened_as(thread, account)
|
||||
if reader is None:
|
||||
on_ui(
|
||||
lambda: _bulletin("Media unavailable: sign in an account of the logger's groups"),
|
||||
600,
|
||||
)
|
||||
|
||||
def _into_thread(self, msg, item, discussion, top, sources):
|
||||
"""Makes a mock comment a message of the discussion thread."""
|
||||
from org.telegram.tgnet import TLRPC
|
||||
|
||||
msg.id = int(item["source_message_id"])
|
||||
msg.dialog_id = -int(discussion)
|
||||
msg.peer_id = TLRPC.TL_peerChannel()
|
||||
msg.peer_id.channel_id = int(discussion)
|
||||
reply = TLRPC.TL_messageReplyHeader()
|
||||
target = int(item.get("reply_to") or 0)
|
||||
reply.reply_to_msg_id = target if target in sources else top
|
||||
reply.reply_to_top_id = top
|
||||
reply.flags |= 2
|
||||
msg.reply_to = reply
|
||||
msg.flags |= 8
|
||||
|
||||
def _start_message(self, original, discussion, top, count):
|
||||
"""The post as its discussion copy (forwarded from the channel), the
|
||||
thread's start message."""
|
||||
from org.telegram.tgnet import TLRPC
|
||||
|
||||
owner = original.messageOwner
|
||||
msg = TLRPC.TL_message()
|
||||
msg.id = top
|
||||
msg.dialog_id = -int(discussion)
|
||||
msg.peer_id = TLRPC.TL_peerChannel()
|
||||
msg.peer_id.channel_id = int(discussion)
|
||||
msg.from_id = owner.peer_id
|
||||
msg.flags |= 256
|
||||
fwd = TLRPC.TL_messageFwdHeader()
|
||||
fwd.from_id = owner.peer_id
|
||||
fwd.channel_post = int(original.getId())
|
||||
fwd.saved_from_peer = owner.peer_id
|
||||
fwd.saved_from_msg_id = int(original.getId())
|
||||
fwd.date = int(owner.date)
|
||||
fwd.flags |= 1 | 4 | 16
|
||||
msg.fwd_from = fwd
|
||||
msg.flags |= 4
|
||||
msg.date = int(owner.date)
|
||||
msg.message = owner.message or ""
|
||||
msg.entities = owner.entities
|
||||
if owner.media is not None:
|
||||
msg.media = owner.media
|
||||
msg.flags |= 512
|
||||
replies = TLRPC.TL_messageReplies()
|
||||
replies.replies = int(count)
|
||||
msg.replies = replies
|
||||
msg.flags |= 1 << 23
|
||||
return msg
|
||||
|
||||
# one comment's content
|
||||
|
||||
def _base(self, original, item):
|
||||
from org.telegram.messenger import MessagesController
|
||||
from org.telegram.tgnet import TLRPC
|
||||
|
||||
owner = original.messageOwner
|
||||
msg = TLRPC.TL_message()
|
||||
msg.id = -int(item.get("source_message_id") or 1)
|
||||
msg.dialog_id = int(original.getDialogId())
|
||||
msg.peer_id = owner.peer_id
|
||||
sender = item.get("sender_id")
|
||||
controller = MessagesController.getInstance(int(original.currentAccount))
|
||||
msg.from_id = _peer(controller, sender) if sender else owner.peer_id
|
||||
msg.flags |= 256
|
||||
msg.out = False
|
||||
msg.date = _epoch(item.get("date")) or int(owner.date)
|
||||
return msg
|
||||
|
||||
def _from_copy(self, original, item, copy):
|
||||
"""The copy without its header (From, links...)."""
|
||||
from java.util import ArrayList
|
||||
|
||||
from hooks.chat import _rich_plain_text, _strip_rich_header
|
||||
|
||||
msg = super()._from_copy(original, item, copy)
|
||||
rich = getattr(copy, "rich_message", None)
|
||||
if rich is not None:
|
||||
_strip_rich_header(rich)
|
||||
plain = _rich_plain_text(rich)
|
||||
if plain is not None: # text only: a normal bubble
|
||||
msg.message = plain
|
||||
msg.entities = ArrayList()
|
||||
msg.rich_message = None
|
||||
msg.flags2 &= ~(1 << 12)
|
||||
return msg
|
||||
text, entities = header.strip(
|
||||
msg.message or "",
|
||||
[(int(e.offset), int(e.length), e) for e in java_list(msg.entities)],
|
||||
)
|
||||
msg.message = text
|
||||
msg.entities = ArrayList()
|
||||
for offset, length, entity in entities:
|
||||
entity.offset = offset
|
||||
entity.length = length
|
||||
msg.entities.add(entity)
|
||||
return msg
|
||||
+29
-14
@@ -217,6 +217,25 @@ class History:
|
||||
# the sheet
|
||||
|
||||
def _show(self, original, versions, reader, found, topic):
|
||||
entries = []
|
||||
last = len(versions) - 1
|
||||
for index, version in enumerate(versions):
|
||||
label = "Original" if version["version"] == 0 else f"Edit {version['version']}"
|
||||
if index == last:
|
||||
label += " · current"
|
||||
when = _when(version.get("edit_date"))
|
||||
entries.append((f"{label} · {when}" if when else label, version))
|
||||
self._sheet(original, "Edit history", entries, reader, found, topic)
|
||||
|
||||
def _message_for(self, original, item, copy, reader):
|
||||
"""(TL message, account to draw it as) of an entry: from its copy in
|
||||
the activity group when readable, else from the server's text."""
|
||||
if copy is not None and not isinstance(copy, _message_empty()):
|
||||
return self._from_copy(original, item, copy), reader
|
||||
return self._from_text(original, item), int(original.currentAccount)
|
||||
|
||||
def _sheet(self, original, title_text, entries, reader, found, topic):
|
||||
"""The mock chat: a service pill (label) and a bubble per entry."""
|
||||
from android.view import Gravity
|
||||
from android.widget import LinearLayout, ScrollView, TextView
|
||||
from client_utils import get_last_fragment
|
||||
@@ -232,21 +251,17 @@ class History:
|
||||
column = LinearLayout(context)
|
||||
column.setOrientation(LinearLayout.VERTICAL)
|
||||
column.setPadding(0, dp(8), 0, dp(12))
|
||||
last = len(versions) - 1
|
||||
for index, version in enumerate(versions):
|
||||
label = "Original" if version["version"] == 0 else f"Edit {version['version']}"
|
||||
if index == last:
|
||||
label += " · current"
|
||||
when = _when(version.get("edit_date"))
|
||||
column.addView(self._service(context, f"{label} · {when}" if when else label))
|
||||
copy = found.get(int(version.get("message_id") or 0)) if reader is not None else None
|
||||
if copy is not None and not isinstance(copy, _message_empty()):
|
||||
msg, account = self._from_copy(original, version, copy), reader
|
||||
else:
|
||||
msg, account = self._from_text(original, version), int(original.currentAccount)
|
||||
for label, item in entries:
|
||||
column.addView(self._service(context, label))
|
||||
copy = (
|
||||
found.get(int(item.get("message_id") or 0)) if reader is not None else None
|
||||
)
|
||||
try:
|
||||
msg, account = self._message_for(original, item, copy, reader)
|
||||
column.addView(
|
||||
self._bubble(context, account, self._message_object(account, original, msg), version)
|
||||
self._bubble(
|
||||
context, account, self._message_object(account, original, msg), item
|
||||
)
|
||||
)
|
||||
except Exception as exc:
|
||||
diag.error("history.bubble", exc)
|
||||
@@ -265,7 +280,7 @@ class History:
|
||||
header.setOrientation(LinearLayout.HORIZONTAL)
|
||||
header.setGravity(Gravity.CENTER_VERTICAL)
|
||||
title = TextView(context)
|
||||
title.setText("Edit history" + ("" if reader is not None else " · media unavailable"))
|
||||
title.setText(title_text + ("" if reader is not None else " · media unavailable"))
|
||||
title.setTextSize(17)
|
||||
title.setTextColor(jint(Theme.getColor(Theme.key_dialogTextBlack)))
|
||||
title.setPadding(dp(20), dp(14), dp(12), dp(12))
|
||||
|
||||
Reference in new issue
Block a user