-
-
Notifications
You must be signed in to change notification settings - Fork 84
/
Copy pathtelega-msg.el
2553 lines (2298 loc) · 111 KB
/
telega-msg.el
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
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
;;; telega-msg.el --- Messages for telega -*- lexical-binding:t -*-
;; Copyright (C) 2018-2024 by Zajcev Evgeny.
;; Author: Zajcev Evgeny <[email protected]>
;; Created: Fri May 4 03:49:22 2018
;; 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/>.
;;; Commentary:
;;
;;; Code:
(require 'format-spec)
(require 'telega-core)
(require 'telega-tdlib)
(require 'telega-customize)
(require 'telega-i18n)
(require 'telega-media)
(require 'telega-ffplay) ; telega-ffplay-run
(require 'telega-vvnote)
(require 'telega-util)
(require 'telega-tme)
(require 'telega-webpage)
(require 'telega-story)
(declare-function telega-root-view--update "telega-root" (on-update-prop &rest args))
(declare-function telega-chat-get "telega-chat" (chat-id &optional offline-p))
(declare-function telega-chat--goto-msg "telega-chat" (chat msg-id &optional highlight callback))
(declare-function telega-msg-redisplay "telega-chat" (msg &optional node))
(declare-function telega-chatbuf--manage-point "telega-chat" (&optional point only-prompt-p))
(declare-function telega-chatbuf--next-msg "telega-chat" (msg msg-temex &optional backward))
(declare-function telega-chatbuf--activate-vvnote-msg "telega-chat" (msg))
(declare-function telega-chat-title "telega-chat" (chat &optional no-badges))
(declare-function telega-chatbuf--node-by-msg-id "telega-chat" (msg-id))
(declare-function telega-chatbuf--chat-update "telega-chat" (&rest dirtiness))
(declare-function telega-chat--type "telega-chat" (chat))
(declare-function telega-chatevent-log-filter "telega-chat" (&rest filters))
(declare-function telega-chat--pop-to-buffer "telega-chat" (chat))
(declare-function telega--full-info "telega-info" (tlobj &optional _callback))
;; Menu for right-mouse on message
(defvar telega-msg-button-menu-map
(let ((menu-map (make-sparse-keymap "Telega Message")))
(bindings--define-key menu-map [telega-msg-mark-toggle]
'(menu-item (telega-i18n "lng_context_select_msg") telega-msg-mark-toggle
:help "Mark the message"
:button (:toggle . (telega-msg-marked-p
(telega-msg-at-down-mouse-3)))))
;; :visible (not (telega-msg-marked-p
;; (telega-msg-at-down-mouse-3)))))
;; (bindings--define-key menu-map [unmark]
;; '(menu-item "Unmark" telega-msg-mark-toggle
;; :help "Unmark the message"
;; :visible (telega-msg-marked-p (telega-msg-at-down-mouse-3))))
(bindings--define-key menu-map [s0] menu-bar-separator)
(bindings--define-key menu-map [add-tags]
'(menu-item (telega-i18n "lng_add_tag_button") telega-msg-add-reaction
:help "Add tag to the message"
:visible (telega-msg-match-p (telega-msg-at-down-mouse-3)
'(chat saved-messages))))
(bindings--define-key menu-map [add-favorite]
'(menu-item "Add to Favorites" telega-msg-favorite-toggle
:help "Add message to the list of favorite messages"
:visible (not (telega-msg-favorite-p
(telega-msg-at-down-mouse-3)))))
(bindings--define-key menu-map [rm-favorite]
'(menu-item "Remove from Favorites" telega-msg-favorite-toggle
:help "Remove message from the list of favorite messages"
:visible (telega-msg-favorite-p (telega-msg-at-down-mouse-3))))
(bindings--define-key menu-map [save]
'(menu-item (telega-i18n "lng_context_save_file") telega-msg-save
:help "Save message's media to a file"))
(bindings--define-key menu-map [copy-link]
'(menu-item (telega-i18n "lng_context_copy_link") telega-msg-copy-link
:help "Copy link to the message to the kill ring"))
(bindings--define-key menu-map [copy-text]
'(menu-item (telega-i18n "lng_context_copy_text") telega-msg-copy-text
:visible (let ((msg (telega-msg-at-down-mouse-3)))
(telega-msg-content-text msg 'with-voice-note))
:help "Copy message text to the kill ring"))
(bindings--define-key menu-map [unpin]
'(menu-item (telega-i18n "lng_context_unpin_msg") telega-msg-pin-toggle
:help "Unpin message"
:visible (let ((msg (telega-msg-at-down-mouse-3)))
(and (telega-chat-match-p (telega-msg-chat msg)
'(my-permission :can_pin_messages))
(plist-get msg :is_pinned)))))
(bindings--define-key menu-map [pin]
'(menu-item (telega-i18n "lng_context_pin_msg") telega-msg-pin-toggle
:help "Pin message"
:visible (let ((msg (telega-msg-at-down-mouse-3)))
(and (telega-chat-match-p (telega-msg-chat msg)
'(my-permission :can_pin_messages))
(not (plist-get msg :is_pinned))))))
(bindings--define-key menu-map [s1] menu-bar-separator)
(bindings--define-key menu-map [ban-sender]
'(menu-item (propertize "Ban Sender" 'face 'error)
telega-msg-ban-sender
:help "Ban/report message sender"
:enable (let ((msg (telega-msg-at-down-mouse-3)))
(telega-chat-match-p (telega-msg-chat msg)
'(my-permission :can_restrict_members)))
))
(bindings--define-key menu-map [delete]
'(menu-item (propertize (telega-i18n "lng_context_delete_msg") 'face 'error)
telega-msg-delete-dwim
:enable (telega-msg-match-p (telega-msg-at-down-mouse-3)
'(or (message-property :can_be_deleted_for_all_users)
(message-property :can_be_deleted_only_for_self)))
))
;; TODO: create submenu for reporting a message/chat/reactions/etc
;; (bindings--define-key menu-map [report]
;; '(menu-item (propertize (telega-i18n "lng_context_report_msg") 'face 'error)
;; telega-msg-report-dwim
;; :enable (telega-msg-match-p (telega-msg-at-down-mouse-3)
;; '(or (message-property :can_be_deleted_for_all_users)
;; (message-property :can_be_deleted_only_for_self)))
;; ))
(bindings--define-key menu-map [forward]
'(menu-item (telega-i18n "lng_context_forward_msg")
telega-msg-forward-dwim
:help "Forward messages"
:enable (telega-msg-match-p (telega-msg-at-down-mouse-3)
'(message-property :can_be_forwarded))
))
(bindings--define-key menu-map [translate]
'(menu-item (telega-i18n "lng_context_translate") telega-msg-translate
:help "Translate message's text"
:visible (telega-msg-content-text
(telega-msg-at-down-mouse-3))))
(bindings--define-key menu-map [s2] menu-bar-separator)
(bindings--define-key menu-map [topic]
'(menu-item (telega-i18n "lng_replies_view_topic")
telega-msg-open-thread-or-topic
:help "Show message's topic"
:visible (telega-msg-match-p (telega-msg-at-down-mouse-3)
'is-topic)))
(bindings--define-key menu-map [thread]
'(menu-item (telega-i18n "lng_replies_view_thread")
telega-msg-open-thread-or-topic
:help "Show message's thread"
:visible (telega-msg-match-p (telega-msg-at-down-mouse-3)
'is-thread)))
(bindings--define-key menu-map [edit]
'(menu-item (telega-i18n "lng_context_edit_msg") telega-msg-edit
:help "Edit the message"
:enable (telega-msg-match-p (telega-msg-at-down-mouse-3)
'(message-property :can_be_edited))))
(bindings--define-key menu-map [reply-another-char]
'(menu-item (telega-i18n "lng_reply_in_another_chat")
telega-msg-reply-in-another-chat
:help (telega-i18n "lng_reply_in_another_chat")
:enable (telega-msg-match-p (telega-msg-at-down-mouse-3)
'(message-property :can_be_replied_in_another_chat))
))
(bindings--define-key menu-map [reply]
'(menu-item (telega-i18n "lng_context_reply_msg") telega-msg-reply
:help "Reply to the message"))
(bindings--define-key menu-map [s3] menu-bar-separator)
(bindings--define-key menu-map [describe]
'(menu-item (telega-i18n "lng_info_about_label") telega-describe-message
:help "Describe the message"))
menu-map))
(defvar telega-msg-button-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map button-map)
(define-key map [remap self-insert-command] #'undefined)
(define-key map (kbd "SPC") 'scroll-up-command)
(define-key map (kbd "c") 'telega-msg-copy-dwim)
(define-key map (kbd "d") 'telega-msg-delete-dwim)
(define-key map (kbd "e") 'telega-msg-edit)
(define-key map (kbd "f") 'telega-msg-forward-dwim)
(define-key map (kbd "i") 'telega-describe-message)
(define-key map (kbd "l") 'telega-msg-copy-link)
;; Marking, `telega-msg-forward' and `telega-msg-delete' can work
;; on list of marked messages
(define-key map (kbd "m") 'telega-msg-mark-toggle)
(define-key map (kbd "n") 'telega-button-forward)
(define-key map (kbd "<tab>") 'telega-chatbuf-next-link)
(define-key map (kbd "p") 'telega-button-backward)
(define-key map (kbd "<backtab>") 'telega-chatbuf-prev-link)
(define-key map (kbd "r") 'telega-msg-reply)
(define-key map (kbd "t") 'telega-msg-translate)
(define-key map (kbd "B") 'telega-msg-ban-sender)
(define-key map (kbd "F") 'telega-msg-forward-dwim-to-many)
(define-key map (kbd "L") 'telega-msg-redisplay)
(define-key map (kbd "P") 'telega-msg-pin-toggle)
(define-key map (kbd "R") 'telega-msg-resend)
(define-key map (kbd "S") 'telega-msg-save)
(define-key map (kbd "T") 'telega-msg-open-thread-or-topic)
(define-key map (kbd "U") 'telega-chatbuf-msg-marks-toggle)
(define-key map (kbd "!") 'telega-msg-add-reaction)
(define-key map (kbd "=") 'telega-msg-diff-edits)
(define-key map (kbd "^") 'telega-msg-pin-toggle)
(define-key map (kbd "DEL") 'telega-msg-delete-dwim)
(define-key map (kbd "*") 'telega-msg-favorite-toggle)
;; Menu for right mouse on a message
(define-key map [down-mouse-3] telega-msg-button-menu-map)
(define-key map [mouse-3] #'ignore)
;; ffplay media controls for some media messages
(define-key map (kbd ",") 'telega-msg--vvnote-rewind-10-backward)
(define-key map (kbd "<") 'telega-msg--vvnote-rewind-10-backward)
(define-key map (kbd ".") 'telega-msg--vvnote-rewind-10-forward)
(define-key map (kbd ">") 'telega-msg--vvnote-rewind-10-forward)
(define-key map (kbd "x") 'telega-msg--vvnote-play-speed-toggle)
(define-key map (kbd "0") 'telega-msg--vvnote-stop)
(define-key map (kbd "1") 'telega-msg--vvnote-rewind-part)
(define-key map (kbd "2") 'telega-msg--vvnote-rewind-part)
(define-key map (kbd "3") 'telega-msg--vvnote-rewind-part)
(define-key map (kbd "4") 'telega-msg--vvnote-rewind-part)
(define-key map (kbd "5") 'telega-msg--vvnote-rewind-part)
(define-key map (kbd "6") 'telega-msg--vvnote-rewind-part)
(define-key map (kbd "7") 'telega-msg--vvnote-rewind-part)
(define-key map (kbd "8") 'telega-msg--vvnote-rewind-part)
(define-key map (kbd "9") 'telega-msg--vvnote-rewind-part)
map))
(defvar telega-msg-button-spoiler-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map telega-msg-button-map)
(define-key map (kbd "RET") 'telega-msg-remove-text-spoiler)
map))
(define-button-type 'telega-msg
:supertype 'telega
:inserter telega-inserter-for-msg-button
:predicate (lambda (button)
(when-let ((msg (telega-msg-at button)))
(not (telega-msg-internal-p msg))))
;; NOTE: To make input method works under message buttons,
;; See `quail-input-method' for details
'read-only t
'front-sticky t
'keymap telega-msg-button-map
'action 'telega-msg-button--action)
(defun telega-msg-button--action (button)
"Action to take when chat BUTTON is pressed."
(let ((msg (telega-msg-at button))
;; If custom `:action' is used for the button, then use it,
;; otherwise open content
(custom-action (button-get button :action)))
(cl-assert msg)
(if custom-action
(funcall custom-action msg)
(telega-msg-open-content msg 'clicked))))
(defun telega-msg-create-internal (chat fmt-text &rest props)
"Create message for internal use.
Used to add content to chatbuf that is not a regular message.
FMT-TEXT is formatted text, can be created with `telega-fmt-text'.
PROPS are additional properties to the internal message."
(declare (indent 1))
(nconc (list :@type "message"
:id -1
:chat_id (plist-get chat :id)
:content (list :@type "telegaInternal"
:text fmt-text))
props))
(defun telega-msg-p (obj)
"Return non-nil if OBJ is a message object."
(and (listp obj) (equal "message" (plist-get obj :@type))))
(defun telega-msg-internal-p (msg)
"Return non-nil if MSG is internal, created with `telega-msg-create-internal'."
(eq -1 (plist-get msg :id)))
(defun telega-msg-from-history-p (msg)
"Return non-nil if MSG belongs to chat history."
;; NOTE: Ignore scheduled/internal messages, see
;; https://github.com/zevlg/telega.el/issues/250
(and msg
(not (telega-msg-internal-p msg))
(not (plist-get msg :sending_state))
(not (plist-get msg :scheduling_state))))
(defun telega-msg-get (chat msg-id &optional callback)
"Get message by CHAT-ID and MSG-ID pair.
If CALLBACK is not specified, then do not perform request to
telega-server, check only in messages cache. If CALLBACK
is specified, it should accept two argument s - MESSAGE and
optional OFFLINE-P, non-nil OFFLINE-P means no request to the
telega-server has been made.
Return a message or nil if CALLBACK is not specified.
Return nil if CALLBACK is specified and message is found without
requests to telega-server, and return `:@extra' value of async request
if request is made."
(declare (indent 2))
;; - Search in the messages cache
;; - [DON'T] Search in chatbuf messages, because message could be
;; offloaded from the cache and will be outdated
;; - [DON'T] Check pinned messages in the chatbuf
(let* ((chat-id (plist-get chat :id))
(msg (gethash (cons chat-id msg-id) telega--cached-messages)))
(if (or msg (null callback))
(if callback
(progn
(funcall callback msg 'offline-p)
nil)
msg)
(cl-assert callback)
(telega--getMessage chat-id msg-id callback))))
(defun telega-msg-at-down-mouse-3 ()
"Return message at down-mouse-3 press.
Return nil if there is no `down-mouse-3' keys in `this-command-keys'."
(when-let* ((ev-key (assq 'down-mouse-3 (append (this-command-keys) nil)))
(ev-start (cadr ev-key))
(ev-point (posn-point ev-start)))
(telega-msg-at ev-point)))
(defun telega-msg-at (&optional pos msg-predicate)
"Return current message at POS point.
If POS is ommited, then return massage at current point.
For interactive commands acting on message at point/mouse-event
use `telega-msg-for-interactive' instead.
If MSG-PREDICATE is specified, return non-nil only if resulting
message matches MSG-PREDICATE."
(let* ((button (button-at (or pos (point))))
(msg (when (and button (eq (button-type button) 'telega-msg))
(button-get button :value))))
(when (or (null msg-predicate)
(and msg (funcall msg-predicate msg)))
msg)))
(defun telega-msg-for-interactive ()
"Return message at mouse event or at current point.
Raise an error if resulting message is telega internal message.
For use by interactive commands."
(when-let ((msg (or (telega-msg-at-down-mouse-3)
(telega-msg-at (point)))))
(when (telega-msg-internal-p msg)
(user-error "Can't operate on internal message"))
msg))
(defun telega-msg-chosen-reaction-types (msg)
"Return reaction chosen by me for the message MSG."
(mapcar (telega--tl-prop :type)
(seq-filter (telega--tl-prop :is_chosen)
(telega--tl-get msg :interaction_info :reactions
:reactions))))
(defun telega-msg-chat (msg &optional offline-p)
"Return chat for the MSG.
Return nil for deleted messages."
(telega-chat-get (plist-get msg :chat_id) offline-p))
(defun telega-msg-replies-count (msg)
"Return number of replies to the message MSG."
(or (telega--tl-get msg :interaction_info :reply_info :reply_count) 0))
(defun telega-msg-replies-has-unread-p (msg)
"Return non-nil if some replies to MSG has been read and there are new unread."
(let* ((reply-info (telega--tl-get msg :interaction_info :reply_info))
(last-msg-id (plist-get reply-info :last_message_id))
(last-read-msg-id (plist-get reply-info :last_read_inbox_message_id)))
(and last-msg-id last-read-msg-id (not (zerop last-read-msg-id))
(< last-read-msg-id last-msg-id))))
(defun telega-msg-goto (msg &optional highlight)
"Goto message MSG."
(telega-chat--goto-msg
(telega-msg-chat msg) (plist-get msg :id) highlight))
(defun telega-msg-goto-highlight (msg)
"Goto message MSG and highlight it."
(telega-msg-goto msg 'highlight))
(defun telega-msg-goto-reply-to-message (msg)
"Goto message denoted by `:reply_to' field of the message MSG."
(let* ((reply-to (plist-get msg :reply_to))
(chat-id (plist-get reply-to :chat_id))
(msg-id (plist-get reply-to :message_id)))
(unless (or (telega-zerop chat-id) (telega-zerop msg-id))
(telega-chat--goto-msg (telega-chat-get chat-id) msg-id 'highlight))))
(defun telega-msg-open-sticker (msg &optional sticker)
"Open content for sticker message MSG."
(unless sticker
(setq sticker (telega--tl-get msg :content :sticker)))
(if (and (not (telega-sticker-static-p sticker))
telega-sticker-animated-play
(not current-prefix-arg))
(telega-sticker--animate sticker)
(let ((sset-id (plist-get sticker :set_id)))
(if (string= "0" sset-id)
(message "Sticker has no associated stickerset")
(if-let ((sset (telega-stickerset-get sset-id 'locally)))
(telega-describe-stickerset sset (telega-msg-chat msg))
(with-telega-help-win "*Telegram Sticker Set*"
(telega-ins "Loading stickerset..."))
(telega-stickerset-get sset-id nil
(lambda (stickerset)
(telega-describe-stickerset
stickerset (telega-msg-chat msg)))))))))
(defun telega-msg-open-animated-emoji (msg &optional clicked-p)
"Open content for animated emoji message MSG."
;; NOTE: Sticker may be nil if yet unknown for a custom emoji
(when-let ((sticker (telega--tl-get msg :content :animated_emoji :sticker)))
;; NOTE: animated message could be a static sticker, such as in
;; the https://t.me/tgbetachat/1319907
(when (and (not (telega-sticker-static-p sticker))
telega-sticker-animated-play)
(telega-sticker--animate sticker msg)))
;; Try fullscreen animated emoji sticker as well
(when clicked-p
(when (eq telega-emoji-animated-play 'with-sound)
(when-let ((sfile (telega--tl-get msg :content :animated_emoji :sound)))
(when (telega-file--downloaded-p sfile)
(telega-ffplay-run (telega--tl-get sfile :local :path)
(cdr (assq 'animated-emoji telega-open-message-ffplay-args))))))
(telega--clickAnimatedEmojiMessage msg
(lambda (fs-sticker)
(when (and (not (telega--tl-error-p fs-sticker))
(not (telega-sticker-static-p fs-sticker))
telega-sticker-animated-play)
(plist-put msg :telega-sticker-fullscreen fs-sticker)
;; TODO: Use tgsplay tool to play fullscreen sticker
(telega-sticker--animate fs-sticker msg))))))
(defun telega-msg-open-story (msg)
"Open content for the forwarded story message MSG."
(let* ((content (plist-get msg :content))
(chat-id (plist-get content :story_sender_chat_id))
(story-id (plist-get content :story_id))
(story (telega-story-get chat-id story-id)))
(telega-story-open story msg)))
(defun telega-msg--play-video (msg file &optional done-callback)
"Start playing video FILE for MSG."
(declare (indent 2))
(if (memq 'video telega-open-message-as-file)
(progn
(cl-assert (telega-file--downloaded-p file))
(telega-open-file (telega--tl-get file :local :path) msg))
(telega-video-player-run
(telega--tl-get file :local :path) msg done-callback)))
(defun telega-msg--play-video-incrementally (msg video _file)
"For massage MSG start playing VIDEO file, while still downloading it."
;; NOTE: search `moov' atom at the beginning or ending
;; Considering 2k of index data per second
(let ((video-file (telega-file--renew video :video)))
(if (and (telega-file--downloaded-p video-file)
(plist-get video :telega-video-pending-open))
(telega-msg--play-video msg video-file)
;; File is partially downloaded, start playing incrementally if:
;; 1) At least 5 seconds of the video is downloaded from the
;; beginning of the file
;; 2) `moov' atom is available, we use
;; `telega-ffplay-get-resolution' function to check this
;; 3) File downloads faster, than it takes time to play
(let* ((fsize (telega-file--size video-file))
(local-file (plist-get video-file :local))
(doffset (plist-get local-file :download_offset))
(psize (plist-get local-file :downloaded_prefix_size))
(dsize (telega-file--downloaded-size video-file))
(duration (or (plist-get video :duration) 50))
(probe-size (plist-get video :telega-video-probe-size))
(open-time (plist-get video :telega-video-pending-open))
;; Size for 10 seconds of the video
(d5-size (/ (* 10 fsize) duration))
;; Downloaded duration
(ddur (* duration (/ (float (- dsize (min dsize (or probe-size 0))))
fsize))))
(when (and ;; Check for 1)
(zerop doffset)
probe-size
(>= psize probe-size)
(> dsize (+ probe-size d5-size))
;; Check for 3)
open-time (> ddur (- (time-to-seconds) open-time)))
;; Check for 2)
(if (not (telega-ffplay-get-resolution
(telega--tl-get video-file :local :path)))
;; NOTE: Can't play this file incrementally, so start
;; playing it after full download
(plist-put video :telega-video-probe-size nil)
;; NOTE: By canceling downloading process we lock
;; filename, so file won't be updated at video player start
;; time. After video player is started, we continue
;; downloading process, canceling it only on video player
;; exit
(plist-put video :telega-video-pending-open nil)
(telega--cancelDownloadFile video-file nil
(lambda (_ignored)
(let ((vfile (telega-file-get (plist-get video-file :id) 'local)))
(telega-msg--play-video msg vfile
(lambda ()
(telega--cancelDownloadFile vfile)))
;; Continue downloading file in 0.5 seconds, giving
;; time for video player command to run
(run-with-timer 0.5 nil #'telega-file--download vfile
:priority 32
:update-callback
(lambda (_ignored)
(telega-msg-redisplay msg)))
)))))))))
(defun telega-msg-open-video (msg &optional video)
"Open content for video message MSG."
(cl-assert (or (and msg (not video))
(eq video (telega--tl-get
msg :content :link_preview :type :video))))
(let* ((video (or video (telega--tl-get msg :content :video)))
(video-file (telega-file--renew video :video))
(incremental-part
(and telega-video-play-incrementally
(plist-get video :supports_streaming)
(not (memq 'video telega-open-message-as-file))
;; Store moov-size at the end to download
(let ((moov-size (* 2 1024 (or (plist-get video :duration) 50)))
(vsize (telega-file--size video-file)))
(when (> vsize moov-size)
(cons (- vsize moov-size) moov-size))))))
(when (telega--tl-get msg :content :is_secret)
(telega--openMessageContent msg))
(cond ((telega-file--downloaded-p video-file)
(telega-msg--play-video msg video-file))
(incremental-part
;; Play video incrementally
(plist-put video :telega-video-pending-open (time-to-seconds))
(plist-put video :telega-video-probe-size (cdr incremental-part))
(telega-file--download video-file
:priority 32
:update-callback
(lambda (dfile)
(telega-msg-redisplay msg)
(telega-msg--play-video-incrementally msg video dfile)))
;; TODO: download incremental-part first, because moov atom
;; might be at the end of the file
;; However, code below does not work for some reason
;; (telega-file--download video-file
;; :priority 32
;; :offset (car incremental-part)
;; :limit (cdr incremental-part)
;; :update-callback
;; (lambda (dfile)
;; (telega-msg-redisplay msg)
;; (unless (telega-file--downloading-p dfile)
;; (telega-msg--play-video-incrementally msg video dfile)
;; ;; Continue downloading whole file
;; (telega-file--download dfile
;; :priority 32
;; :update-callback
;; (lambda (ddfile)
;; (telega-msg-redisplay msg)
;; (telega-msg--play-video-incrementally msg video ddfile)))
;; )))
)
(t
(telega-file--download video-file
:priority 32
:update-callback
(lambda (dfile)
(telega-msg-redisplay msg)
(when (telega-file--downloaded-p dfile)
(telega-msg--play-video msg dfile))))))))
(defun telega-msg-open-audio (msg &optional audio)
"Open content for audio message MSG."
(cl-assert (or (not audio)
(eq audio (telega--tl-get
msg :content :link_preview :type :audio))))
;; - If already playing, then pause
;; - If paused, start from paused position
;; - If not started, start playing
(let* ((audio (or audio (telega--tl-get msg :content :audio)))
(audio-file (telega-file--renew audio :audio))
(proc (plist-get msg :telega-ffplay-proc))
(paused-p (or telega-ffplay-media-timestamp
(telega-ffplay-paused-p proc))))
(if (telega-ffplay-playing-p proc)
(telega-ffplay-pause proc)
(telega-file--download audio-file
:priority 32
:update-callback
(lambda (file)
(telega-msg-redisplay msg)
(when (telega-file--downloaded-p file)
(if (memq 'audio telega-open-message-as-file)
(telega-open-file (telega--tl-get file :local :path) msg)
(plist-put msg :telega-ffplay-proc
(telega-ffplay-run (telega--tl-get file :local :path)
(concat
(when paused-p
(format "-ss %.2f " paused-p))
(cdr (assq 'audio telega-open-message-ffplay-args)))
(lambda (_proc)
(telega-msg-redisplay msg))
paused-p)))))))))
(defun telega-msg-voice-note--ffplay-callback (proc msg &optional
no-progress-adjust)
"Callback for voice/video note.
Adjust progress according to the `telega-vvnote-play-speed'.
Also, start playing next voice/video note when active voice/video
note finishes."
;; NOTE: adjust `:progress' with `telega-vvnote-play-speed'
;; since ffplay reports progress disreguarding atempo
(unless no-progress-adjust
(when-let* ((proc-plist (process-plist proc))
(progress (plist-get proc-plist :progress))
(resumed-at (or (plist-get msg :telega-ffplay-resumed-at) 0)))
(cl-assert (numberp progress))
(unless (equal 1 telega-vvnote-play-speed)
(setq progress (+ resumed-at (* (- progress resumed-at)
telega-vvnote-play-speed)))
(set-process-plist proc (plist-put proc-plist :progress progress)))))
(telega-msg-redisplay msg)
(unless (process-live-p proc)
;; NOTE: another message could be already activated
(with-telega-chatbuf (telega-msg-chat msg)
(when (eq telega-chatbuf--vvnote-msg msg)
(telega-chatbuf--activate-vvnote-msg nil))))
(when (and telega-vvnote-play-next
(eq 'finished (telega-ffplay-stop-reason proc)))
;; NOTE: ffplay exited normally (finished playing), try to play
;; next voice/video message if any
(when-let ((next-vvnote-msg (telega-chatbuf--next-msg msg
'(type VoiceNote VideoNote))))
(with-telega-chatbuf (telega-msg-chat next-vvnote-msg)
(telega-chatbuf--goto-msg (plist-get next-vvnote-msg :id) 'highlight))
(telega-msg-open-content next-vvnote-msg))))
(defun telega-msg-open-voice-note (msg)
"Open content for voiceNote message MSG."
;; - If already playing, then pause
;; - If paused, start from paused position
;; - If not started, start playing
(let* ((note (or (telega--tl-get msg :content :voice_note)
(telega--tl-get msg :content :link_preview :type :voice_note)))
(note-file (progn
(cl-assert note)
(telega-file--renew note :voice)))
(proc (plist-get msg :telega-ffplay-proc))
(paused-p (or telega-ffplay-media-timestamp
(telega-ffplay-paused-p proc))))
(if (telega-ffplay-playing-p proc)
(telega-ffplay-pause proc)
;; Start playing or resume from the paused moment
(telega-file--download note-file
:priority 32
:update-callback
(lambda (file)
(cond
((not (telega-file--downloaded-p file))
;; no-op
)
((memq 'voice-note telega-open-message-as-file)
(telega-open-file (telega--tl-get file :local :path) msg))
(t
;; NOTE: Set moment we resumed for `:progress' correction
;; in the `telega-msg-voice-note--ffplay-callback'
(plist-put msg :telega-ffplay-resumed-at paused-p)
(plist-put msg :telega-ffplay-proc
(telega-ffplay-run (telega--tl-get file :local :path)
(concat
(when paused-p
(format "-ss %.2f " paused-p))
(unless (equal telega-vvnote-play-speed 1)
(format "-af atempo=%.2f "
telega-vvnote-play-speed))
(cdr (assq 'voice-note
telega-open-message-ffplay-args)))
(lambda (proc)
(telega-msg-voice-note--ffplay-callback proc msg))
paused-p))
(with-telega-chatbuf (telega-msg-chat msg)
(telega-chatbuf--activate-vvnote-msg msg))))
;; NOTE: always redisplay the message to actualize
;; downloading progress
(telega-msg-redisplay msg))))))
(defun telega-msg-video-note--ffplay-callback (proc frame msg)
"Callback for video note playback."
(let* ((proc-plist (process-plist proc))
(note (or (telega--tl-get msg :content :video_note)
(telega--tl-get msg :content :link_preview :type :video_note)))
(duration (plist-get note :duration))
(nframes (or (float (plist-get proc-plist :nframes))
(* 30.0 duration)))
(played (when frame
(+ (or (plist-get msg :telega-ffplay-resumed-at) 0)
(* (/ (car frame) nframes) duration))))
(normalized-progress
(when frame
(/ (or played 0) (if (zerop duration) 0.1 duration))))
(ffplay-frame
(when frame
(telega-vvnote-video--svg (cdr frame)
:progress (if (telega-ffplay-paused-p proc)
(cons 'paused normalized-progress)
normalized-progress)))))
;; NOTE: Update proc's `:progress' property to start from correct
;; place if [x2] button is pressed
(set-process-plist proc (plist-put proc-plist :progress played))
;; NOTE: Scale frame when starting to play, simulating Video
;; Messages 2.0 interface in official client
(when (and ffplay-frame (consp telega-video-note-height))
(let* ((max-scale (/ (float (cdr telega-video-note-height))
(float (car telega-video-note-height))))
(scale (if (plist-get msg :telega-ffplay-resumed-at)
max-scale
(min max-scale (+ 1.0 (/ (float (car frame)) 10))))))
(plist-put (cdr ffplay-frame) :scale scale)))
(plist-put msg :telega-ffplay-frame ffplay-frame)
(telega-msg-voice-note--ffplay-callback proc msg 'no-progress-adjust)))
(defun telega-msg-open-video-note (msg)
"Open content for videoNote message MSG.
If called with `\\[universal-argument]' prefix, then open with
external player even if `telega-video-note-play-inline' is
non-nil."
(let* ((note (or (telega--tl-get msg :content :video_note)
(telega--tl-get msg :content :link_preview :type :video_note)))
(note-file (telega-file--renew note :video))
(proc (plist-get msg :telega-ffplay-proc))
(paused-p (or telega-ffplay-media-timestamp
(telega-ffplay-paused-p proc)))
(saved-this-command this-command)
(saved-current-prefix-arg current-prefix-arg))
(if (telega-ffplay-playing-p proc)
(telega-ffplay-pause proc)
(telega-file--download note-file
:priority 32
:update-callback
(lambda (file)
(cond
((not (telega-file--downloaded-p file))
;; no-op
)
((memq 'video-note telega-open-message-as-file)
(telega-open-file (telega--tl-get file :local :path) msg))
((and telega-video-note-play-inline
;; *NOT* called interactively
(or (not saved-this-command)
(not saved-current-prefix-arg)))
;; NOTE: Set moment we resumed for `:progress' correction
;; in the `telega-msg-video-note--ffplay-callback'
(plist-put msg :telega-ffplay-resumed-at paused-p)
(plist-put msg :telega-ffplay-proc
(telega-ffplay-to-png
(telega--tl-get file :local :path)
(concat
"-vf scale=120:120"
(unless (equal telega-vvnote-play-speed 1)
(format " -af atempo=%.2f"
telega-vvnote-play-speed))
(concat " -f " (car telega-vvnote--has-audio-inputs))
" default -vsync 0")
(list #'telega-msg-video-note--ffplay-callback msg)
:seek paused-p :speed telega-vvnote-play-speed))
(with-telega-chatbuf (telega-msg-chat msg)
(telega-chatbuf--activate-vvnote-msg msg)))
(t
(telega-ffplay-run (telega--tl-get file :local :path)
(cdr (assq 'video-note telega-open-message-ffplay-args)))))
;; NOTE: always redisplay the message to actualize
;; downloading progress
(telega-msg-redisplay msg))))))
(defun telega-msg-open-photo (msg &optional photo)
"Open content for photo message MSG."
(telega-photo--open (or photo (telega--tl-get msg :content :photo)) msg))
(defun telega-animation--ffplay-callback (_proc frame anim)
"Callback for inline animation playback."
(plist-put anim :telega-ffplay-frame-filename (cdr frame))
;; NOTE: just redisplay the image, not redisplaying full message
(telega-media--image-update
(cons anim 'telega-animation--create-image) nil)
(force-window-update)
)
(defun telega-msg-open-animation (msg &optional animation)
"Open content for animation message MSG.
If called with `\\[universal-argument]' prefix, then open with
external player even if `telega-animation-play-inline' is
non-nil."
(let* ((anim (or animation
(telega--tl-get msg :content :animation)
(telega--tl-get msg :content :link_preview :type :animation)))
(anim-file (telega-file--renew anim :animation))
(proc (plist-get msg :telega-ffplay-proc))
(saved-this-command this-command)
(saved-current-prefix-arg current-prefix-arg))
(if (telega-ffplay-playing-p proc)
(telega-ffplay-stop proc)
(telega-file--download anim-file
:priority 32
:update-callback
(lambda (file)
(cond
((not (telega-file--downloaded-p file))
;; no-op
)
((and (telega-animation-play-inline-p anim)
(or (not saved-this-command) ; *NOT* called interactively
(not saved-current-prefix-arg)))
(plist-put msg :telega-ffplay-proc
;; NOTE: "-an" for no sound
(telega-ffplay-to-png
(telega--tl-get file :local :path) "-an"
(list #'telega-animation--ffplay-callback anim))))
((memq 'animation telega-open-message-as-file)
(telega-open-file (telega--tl-get file :local :path) msg))
(t
(telega-ffplay-run (telega--tl-get file :local :path)
(cdr (assq 'animation telega-open-message-ffplay-args)))))
;; NOTE: always redisplay the message to actualize
;; downloading progress
(telega-msg-redisplay msg))))))
(defun telega-msg-open-document (msg &optional document)
"Open content for document message MSG."
(let* ((doc (or document (telega--tl-get msg :content :document)))
(doc-file (telega-file--renew doc :document)))
(telega-file--download doc-file
:priority 32
:update-callback
(lambda (file)
(telega-msg-redisplay msg)
(when (telega-file--downloaded-p file)
(telega-open-file (telega--tl-get file :local :path) msg))))))
(defun telega-msg-open-location (msg)
"Open content for location message MSG."
(let* ((loc (telega--tl-get msg :content :location))
(lat (plist-get loc :latitude))
(lon (plist-get loc :longitude))
(url (format-spec telega-location-url-format
(format-spec-make ?N lat ?E lon))))
(telega-browse-url url 'in-web-browser)))
(defun telega-msg-open-contact (msg)
"Open content for contact message MSG."
(telega-describe-contact
(telega--tl-get msg :content :contact)))
(defun telega-msg-open-link-preview (msg &optional link-preview)
"Open content for message with webpage message MSG."
(unless link-preview
(setq link-preview (telega--tl-get msg :content :link_preview)))
(if (telega-zerop (plist-get link-preview :instant_view_version))
(let ((lp-type (plist-get link-preview :type)))
(cl-case (telega--tl-type lp-type)
(linkPreviewTypePhoto
(telega-msg-open-photo msg (plist-get lp-type :photo)))
(linkPreviewTypeVideo
(telega-msg-open-video msg (plist-get lp-type :video)))
(linkPreviewTypeVoiceNote
(telega-msg-open-voice-note msg))
(linkPreviewTypeVideoNote
(telega-msg-open-video-note msg))
((linkPreviewTypeBackground
linkPreviewTypeDocument)
(telega-msg-open-document msg (plist-get lp-type :document)))
((linkPreviewTypeEmbeddedAnimationPlayer
linkPreviewTypeEmbeddedAudioPlayer
linkPreviewTypeEmbeddedVideoPlayer
linkPreviewTypeExternalAudio
linkPreviewTypeExternalVideo
linkPreviewTypeArticle)
;; External link
(telega-browse-url (plist-get link-preview :url)))
(t
;; Internal link
(telega-tme-open (plist-get link-preview :url)))))
(telega-webpage--instant-view
(telega-tl-str link-preview :url) (plist-get link-preview :site_name))))
;; ;; NOTE: "document" webpage might contain :video instead of :document
;; ;; see https://t.me/c/1347510619/43
;; (cond ((plist-get link-preview :animation)
;; (telega-msg-open-animation msg (plist-get link-preview :animation)))
;; ((plist-get link-preview :audio)
;; (telega-msg-open-audio msg (plist-get link-preview :audio)))
;; ((plist-get link-preview :document)
;; (telega-msg-open-document msg (plist-get link-preview :document)))
;; ((plist-get link-preview :sticker)
;; (telega-msg-open-sticker msg (plist-get link-preview :sticker)))
;; ((plist-get link-preview :video)
;; (telega-msg-open-video msg (plist-get link-preview :video)))
;; ((plist-get link-preview :video_note)
;; (telega-msg-open-video-note msg (plist-get link-preview :video_note)))
;; ((and (string= "photo" (plist-get link-preview :type))
;; (plist-get link-preview :photo))
;; (telega-msg-open-photo msg (plist-get link-preview :photo)))
;; (t
;; (when-let ((url (telega-tl-str link-preview :url)))
;; (telega-browse-url url)))))
(defun telega-msg-open-game (msg)
"Open content for the game message MSG."
(telega--getCallbackQueryAnswer
msg (list :@type "callbackQueryPayloadGame"
:game_short_name (telega--tl-get msg :content :game :short_name))))
(defun telega-msg-open-poll (msg)
"Open content for the poll MSG."
(let ((poll (telega--tl-get msg :content :poll)))
(unless (plist-get poll :is_anonymous)
(with-telega-help-win "*Telega Poll Results*"
(telega-ins--with-face 'bold
(telega-ins--fmt-text (plist-get poll :question))
(telega-ins " (" (telega-i18n "lng_polls_votes_count"
:count (plist-get poll :total_voter_count))
")"))
(telega-ins "\n")
;; Quiz explanation goes next
(when-let ((explanation (telega--tl-get poll :type :explanation))
(label (propertize
(concat (telega-i18n "lng_polls_solution_title") ": ")
'face 'telega-shadow)))
(telega-ins--labeled label nil
(telega-ins--fmt-text explanation)
(telega-ins "\n")))
(telega-ins "\n")
(let ((options (plist-get poll :options)))
(dotimes (popt-id (length options))
(let ((popt (seq-elt options popt-id)))
(telega-ins--fmt-text (plist-get popt :text))
(telega-ins--with-face 'telega-shadow
(telega-ins-fmt " — %d%% (%s)\n"
(plist-get popt :vote_percentage)
(telega-i18n "lng_polls_votes_count"
:count (plist-get popt :voter_count))))
(when-let* ((voters-reply (telega--getPollVoters msg popt-id))
(voters (mapcar #'telega-msg-sender
(plist-get voters-reply :senders))))
(telega-ins--line-wrap-prefix " "
(seq-doseq (voter voters)
(telega-ins--raw-button
(telega-link-props 'sender voter 'type 'telega)