-
-
Notifications
You must be signed in to change notification settings - Fork 94
Expand file tree
/
Copy pathtelega-topic.el
More file actions
336 lines (290 loc) · 12.8 KB
/
telega-topic.el
File metadata and controls
336 lines (290 loc) · 12.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
;;; telega-topic.el --- Forums support for telega -*- lexical-binding: t -*-
;; Copyright (C) 2022 by Zajcev Evgeny.
;; Author: Zajcev Evgeny <zevlg@yandex.ru>
;; Created: Sun Nov 6 02:12:07 2022
;; Keywords:
;; telega is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; telega is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with telega. If not, see <http://www.gnu.org/licenses/>.
;;; ellit-org: commentary
;;
;; Telegram allows creating forums with multiple distinct topics. Use
;; {{{kbd(M-x telega-chat-create RET forum RET)}}} to create forums.
;;
;; NOTE: forums and topics are not fully supported by =telega= at moment.
;;; Code:
(require 'telega-core)
(declare-function telega-chat--mark-dirty "telega-tdlib-events" (chat &optional event))
(declare-function telega-chat-get "telega-chat" (chat-id &optional offline-p))
(declare-function telega--getForumTopic "telega-tdlib" (chat forum-topic-id &optional callback))
(declare-function telega-topic-button-action "telega-root" (topic))
(defvar telega-topic--default-icons nil
"Cached list of topic icons which can be used by all users.")
(defun telega-forum-topic-icon-custom-emoji-id (forum-topic)
"Return custom emoji id for the FORUM-TOPIC."
(let ((cid (telega--tl-get forum-topic :info :icon :custom_emoji_id)))
(unless (equal "0" cid)
cid)))
(defun telega-forum-topic-color (forum-topic)
"Return color of the FORUM-TOPIC icon."
(format "#%06x" (telega--tl-get forum-topic :info :icon :color)))
(defun telega-forum-topic-avatar-image (topic &optional cheight)
"Return avatar image for the TOPIC."
(if-let* ((cid (telega-forum-topic-icon-custom-emoji-id topic))
(sticker (telega-custom-emoji-get cid)))
(telega-sticker--image sticker)
;; Fallback to svg icon
(let* ((cheight (or cheight 1))
(xh (telega-chars-xheight 1))
(xw (telega-chars-xwidth 2))
(svg (telega-svg-create xw xh))
(title (telega-tl-str (plist-get topic :info) :name))
(general-p (telega-topic-match-p topic 'is-general))
(badge (if general-p "#" (substring title 0 1)))
(font-size (if general-p xh (/ xh 2))))
(unless general-p
;; Draw topic icon
(let* ((color (telega-forum-topic-color topic))
(co (telega-color-name-as-hex-2digits
(color-darken-name color 40)))
(c1 (telega-color-name-as-hex-2digits
(color-darken-name color 15)))
(c2 (telega-color-name-as-hex-2digits
(color-darken-name color 30))))
(svg-gradient svg "cgrad" 'linear (list (cons 0 c1) (cons xh c2)))
(telega-svg-forum-topic-icon svg xw
:stroke-width (/ xh 20.0)
:stroke-color co
:gradient "cgrad")))
(svg-text svg badge
:font-size font-size
:font-weight "bold"
:fill (if general-p
(telega-color-name-as-hex-2digits
(face-foreground 'telega-shadow nil t))
"white")
:font-family "monospace"
:x "50%"
:text-anchor "middle"
;; XXX insane Y calculation
:y (+ (/ font-size 3) (/ xw 2)))
(telega-svg-image svg
:scale 1.0
:width (telega-cw-width (* 2 cheight))
:max-height (telega-ch-height cheight)
:ascent 'center
:mask 'heuristic))))
(defun telega-topic-notification-setting (topic setting)
(telega-chat-notification-setting (telega-topic-chat topic) setting topic))
(defun telega-topic-muted-p (topic)
"Return non-nil if TOPIC is muted."
(not (telega-zerop (telega-topic-notification-setting topic :mute_for))))
(defun telega--MessageTopic-id (msg-topic)
"Return id of the MessageTopic MSG-TOPIC."
(cl-ecase (telega--tl-type msg-topic)
(messageTopicThread
(plist-get msg-topic :message_thread_id))
(messageTopicForum
(plist-get msg-topic :forum_topic_id))
(messageTopicDirectMessages
(plist-get msg-topic :direct_messages_chat_topic_id))
(messageTopicSavedMessages
(plist-get msg-topic :saved_messages_topic_id))))
(defun telega-topic-id (topic)
"Return TOPIC's id."
(cl-ecase (telega--tl-type topic)
(messageThreadInfo
(plist-get topic :message_thread_id))
((savedMessagesTopic directMessagesChatTopic)
(plist-get topic :id))
(forumTopic
(telega--tl-get topic :info :forum_topic_id))))
(defun telega-topic-chat-id (topic)
"Return TOPIC's chat id."
(cl-ecase (telega--tl-type topic)
(savedMessagesTopic
telega--me-id)
((messageThreadInfo directMessagesChatTopic)
(plist-get topic :chat_id))
(forumTopic
(telega--tl-get topic :info :chat_id))))
(defun telega-topic--MessageSource (topic)
"Return MessageSource structure for the TOPIC."
(cl-ecase (telega--tl-type topic)
(messageThreadInfo
'(:@type "messageSourceMessageThreadHistory"))
(savedMessagesTopic
'(:@type "messageSourceOther"))
(directMessagesChatTopic
'(:@type "messageSourceDirectMessagesChatTopicHistory"))
(forumTopic
'(:@type "messageSourceForumTopicHistory"))))
(defun telega-topic-chat (topic)
"Return chat for the TOPIC."
(or (plist-get topic :telega-chat)
(telega-chat-get (telega-topic-chat-id topic))))
(defun telega-topic-get (chat topic-id)
"Return topic by TOPIC-ID in the CHAT."
(alist-get topic-id (telega-chat-topics-alist chat)))
(defun telega-topic-brackets (topic)
"Return pair of brackets to use for TOPIC."
(or (when (eq 'savedMessagesTopic (telega--tl-type topic))
(let ((smtype (plist-get topic :type)))
(when (eq 'savedMessagesTopicTypeSavedFromChat
(telega--tl-type smtype))
(telega-msg-sender-brackets
(telega-chat-get (plist-get smtype :chat_id) 'offline)))))
telega-topic-brackets))
(defun telega-topic--ensure (topic &optional chat)
"Ensure TOPIC for CHAT is stored in the `telega--chat-topics'."
(unless chat
(setq chat (telega-topic-chat topic)))
(if-let ((existing-topic (telega-topic-get chat (telega-topic-id topic))))
;; Update topic inplace
(setcdr existing-topic (cdr topic))
(setf (alist-get (telega-topic-id topic) (telega-chat-topics-alist chat))
topic))
(when (eq 'forumTopic (telega--tl-type topic))
(telega-chat--forum-topics-icons-fetch chat topic))
;; Store back reference to chat in the `:telega-chat' property
(plist-put topic :telega-chat chat)
topic)
(defun telega-chat--forum-topics-fetch (chat &optional callback)
"Fetch CHAT topics in async manner."
(declare (indent 1))
(let ((cb (lambda (reply)
(plist-put chat :telega_topics_count
(plist-get reply :total_count))
(seq-doseq (topic (plist-get reply :topics))
(telega-topic--ensure topic))
(telega-chat--mark-dirty chat 'topics)
(when callback
(funcall callback reply)))))
(telega--getForumTopics chat ""
:callback cb)))
(defun telega-chat--forum-topics-icons-fetch (chat &rest forum-topics)
"Asynchronously fetch icons for the list of the FORUM-TOPICS."
(when-let ((custom-emoji-ids
;; NOTE: Fetch only uncached custom emojis
(seq-remove (lambda (cid)
(or (equal "0" cid) (telega-custom-emoji-get cid)))
(mapcar (telega--tl-prop :info :icon :custom_emoji_id)
forum-topics))))
(telega--getCustomEmojiStickers custom-emoji-ids
(lambda (stickers)
(seq-doseq (sticker stickers)
(telega-custom-emoji--ensure sticker))
(telega-chat--mark-dirty chat)))
))
(defun telega-chat--topics-load (chat &optional callback)
"Load topics for the CHAT in async manner."
(cond ((telega-chat-match-p chat 'saved-messages)
(telega--loadSavedMessagesTopics
(plist-get telega--options
:pinned_saved_messages_topic_count_max)
callback))
((telega-chat-match-p chat 'is-direct-messages-group)
(telega--loadDirectMessagesChatTopics chat
nil callback))
((telega-chat-match-p chat 'is-forum)
(telega-chat--forum-topics-fetch chat callback)
)))
(defun telega-msg-topic (msg &optional sync-p)
"Return topic for the message MSG.
If SYNC-P is non-nil, fetch missing forum topic info before returning it."
(when-let ((msg-topic (plist-get msg :topic_id))
(chat (telega-msg-chat msg 'offline)))
(or (telega-topic-get chat (telega--MessageTopic-id msg-topic))
(when-let* (sync-p
(msg-forum-topic (telega-msg-match-p msg 'is-forum-topic))
(topic (telega--getForumTopic
chat (plist-get msg-forum-topic :forum_topic_id))))
(cl-assert (not (telega--tl-error-p topic)))
(telega-topic--ensure topic chat)))))
(defun telega-topic-at (&optional pos)
"Return topic at point POS."
(let ((button (button-at (or pos (point)))))
(when (and button (eq (button-type button) 'telega-topic))
(button-get button :value))))
(defun telega-topic-goto (topic &optional start-msg-id)
"Open TOPIC in a chatbuf.
If START-MSG-ID is specified, jump to the this message in the topic."
(let* ((topic-chat (telega-topic-chat topic))
(buffer (telega-chatbuf--get-create topic-chat :no-history)))
;; NOTE: pop to buffer after starting filtering by topic, to make
;; `telega-root--keep-cursor-at-chat' do the job for keeping
;; cursor at topic position
(with-current-buffer buffer
(telega-chatbuf-filter-by-topic topic start-msg-id))
(telega-chat--pop-to-buffer topic-chat :no-history)))
(defun telega-describe-topic (topic)
"Show info about TOPIC."
(interactive (list (telega-topic-at (point))))
(with-telega-help-win "*Telegram Topic Info*"
(let ((chat (telega-topic-chat topic))
(topic-info (plist-get topic :info)))
(telega-ins-describe-item (telega-i18n "lng_forum_topic_title")
(telega-ins--with-face 'telega-shadow
(telega-ins (telega-symbol 'topic))
(telega-ins--topic-title topic
:with-icon-p t
:with-maybe-pin-p t))
(telega-ins " ")
;; TODO: [Open] button
;; (telega-ins--ui-button "Open"
;; )
)
(telega-ins-describe-item "Chat"
(telega-button--insert 'telega-chat chat
:inserter #'telega-ins--chat
:action #'telega-chat-button-action))
(telega-ins-describe-item (telega-i18n "lng_link_header_short")
(let* ((msg-link (telega--getForumTopicLink chat topic))
(link-url (telega-tl-str msg-link :link)))
(telega-ins--raw-button
(telega-link-props 'url link-url 'face 'telega-link)
(telega-ins link-url))))
(telega-ins-describe-item "Created"
(telega-ins--date (plist-get topic-info :creation_date) 'date-time))
(telega-ins-describe-item (telega-i18n "lng_topic_author_badge")
(telega-ins--msg-sender
(telega-msg-sender (plist-get topic-info :creator_id))
:with-avatar-p t
:with-username-p t
:with-brackets-p t))
(telega-ins-describe-item "Last-Read-Outbox"
(telega-ins-fmt "%S" (plist-get topic :last_read_outbox_message_id)))
;; TODO: more fields
(when (and (listp telega-debug) (memq 'info telega-debug))
(let ((print-length nil))
(telega-ins "\n---DEBUG---\n")
(telega-ins-fmt "TopicSexp: (telega-topic-get (telega-chat-get %d) %d)\n"
(plist-get chat :id) (telega-topic-id topic))
))
)))
(defun telega-msg-show-topic-info (msg)
"Show MSG's topic info."
(interactive (list (telega-msg-for-interactive)))
(telega-describe-topic (telega-msg-topic msg 'sync)))
;;; Topic button
(defvar telega-topic-button-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map button-map)
(define-key map (kbd "i") 'telega-describe-topic)
(define-key map (kbd "h") 'telega-describe-topic)
map)
"The key map for telega topic buttons.")
(define-button-type 'telega-topic
:supertype 'telega
:inserter telega-inserter-for-topic-button
:action #'telega-topic-button-action
'keymap telega-topic-button-map)
(provide 'telega-topic)
;;; telega-topic.el ends here