branch: elpa/fedi
commit b1d3d6f02ab008528585837dfd0dd0f41bff685b
Author: marty hiatt <martianhiatus [a t] riseup [d o t] net>
Commit: marty hiatt <martianhiatus [a t] riseup [d o t] net>

    timestamp logic from mastodon.el
---
 fedi.el | 212 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 212 insertions(+)

diff --git a/fedi.el b/fedi.el
index afe262f67bf..876afff037b 100644
--- a/fedi.el
+++ b/fedi.el
@@ -294,6 +294,218 @@ NAME is not part of the symbol table, '?' is returned."
           (string-match "^/post/[[:digit:]]+$" query)
           (string-match "^/comment/[[:digit:]]+$" query)))))
 
+;;; TIMESTAMPS
+
+(defvar-local fedi-timestamp-next-update nil
+  "The timestamp when the buffer should next be scanned to update the 
timestamps.")
+
+(defvar-local fedi-timestamp-update-timer nil
+  "The timer that, when set will scan the buffer to update the timestamps.")
+
+(defcustom fedi-enable-relative-timestamps t
+  "Whether to show relative (to the current time) timestamps.
+This will require periodic updates of a timeline buffer to
+keep the timestamps current as time progresses."
+  :type '(boolean :tag "Enable relative timestamps and background updater 
task"))
+
+(defun fedi--find-property-range (property start-point
+                                           &optional search-backwards)
+  "Return nil if no such range is found.
+If PROPERTY is set at START-POINT returns a range around
+START-POINT otherwise before/after START-POINT.
+SEARCH-BACKWARDS determines whether we pick point
+before (non-nil) or after (nil)"
+  (if (get-text-property start-point property)
+      ;; We are within a range, so look backwards for the start:
+      (cons (previous-single-property-change
+             (if (equal start-point (point-max)) start-point (1+ start-point))
+             property nil (point-min))
+            (next-single-property-change start-point property nil (point-max)))
+    (if search-backwards
+        (let* ((end (or (previous-single-property-change
+                         (if (equal start-point (point-max))
+                             start-point (1+ start-point))
+                         property)
+                        ;; we may either be just before the range or there
+                        ;; is nothing at all
+                        (and (not (equal start-point (point-min)))
+                             (get-text-property (1- start-point) property)
+                             start-point)))
+               (start (and end (previous-single-property-change
+                                end property nil (point-min)))))
+          (when end
+            (cons start end)))
+      (let* ((start (next-single-property-change start-point property))
+             (end (and start (next-single-property-change
+                              start property nil (point-max)))))
+        (when start
+          (cons start end))))))
+
+(defun fedi--find-next-or-previous-property-range
+    (property start-point search-backwards)
+  "Find (start . end) property range after/before START-POINT.
+Does so while PROPERTY is set to a consistent value (different
+from the value at START-POINT if that is set).
+Return nil if no such range exists.
+If SEARCH-BACKWARDS is non-nil it find a region before
+START-POINT otherwise after START-POINT."
+  (if (get-text-property start-point property)
+      ;; We are within a range, we need to start the search from
+      ;; before/after this range:
+      (let ((current-range (fedi--find-property-range property start-point)))
+        (if search-backwards
+            (unless (equal (car current-range) (point-min))
+              (fedi--find-property-range
+               property (1- (car current-range)) search-backwards))
+          (unless (equal (cdr current-range) (point-max))
+            (fedi--find-property-range
+             property (1+ (cdr current-range)) search-backwards))))
+    ;; If we are not within a range, we can just defer to
+    ;; fedi--find-property-range directly.
+    (fedi--find-property-range property start-point search-backwards)))
+
+(defun fedi--consider-timestamp-for-updates (timestamp)
+  "Take note that TIMESTAMP is used in buffer and ajust timers as needed.
+This calculates the next time the text for TIMESTAMP will change
+and may adjust existing or future timer runs should that time
+before current plans to run the update function.
+The adjustment is only made if it is significantly (a few
+seconds) before the currently scheduled time. This helps reduce
+the number of occasions where we schedule an update only to
+schedule the next one on completion to be within a few seconds.
+If relative timestamps are disabled (i.e. if
+`mastodon-tl--enable-relative-timestamps' is nil), this is a
+no-op."
+  (when fedi-enable-relative-timestamps
+    (let ((this-update (cdr (fedi--relative-time-details timestamp))))
+      (when (time-less-p this-update
+                         (time-subtract fedi-timestamp-next-update
+                                        (seconds-to-time 10)))
+        (setq fedi-timestamp-next-update this-update)
+        (when fedi-timestamp-update-timer
+          ;; We need to re-schedule for an earlier time
+          (cancel-timer fedi-timestamp-update-timer)
+          (setq fedi-timestamp-update-timer
+                (run-at-time (time-to-seconds (time-subtract this-update
+                                                             (current-time)))
+                             nil ;; don't repeat
+                             #'fedi--update-timestamps-callback
+                             (current-buffer) nil)))))))
+
+(defun fedi--update-timestamps-callback (buffer previous-marker)
+  "Update the next few timestamp displays in BUFFER.
+Start searching for more timestamps from PREVIOUS-MARKER or
+from the start if it is nil."
+  ;; only do things if the buffer hasn't been killed in the meantime
+  (when (and fedi-enable-relative-timestamps ; just in case
+             (buffer-live-p buffer))
+    (save-excursion
+      (with-current-buffer buffer
+        (let ((previous-timestamp (if previous-marker
+                                      (marker-position previous-marker)
+                                    (point-min)))
+              (iteration 0)
+              next-timestamp-range)
+          (if previous-marker
+              ;; a follow-up call to process the next batch of timestamps.
+              ;; Release the marker to not slow things down.
+              (set-marker previous-marker nil)
+            ;; Otherwise this is a rew run, so let's initialize the next-run 
time.
+            (setq fedi-timestamp-next-update (time-add (current-time)
+                                                       (seconds-to-time 300))
+                  fedi-timestamp-update-timer nil))
+          (while (and (< iteration 5)
+                      (setq next-timestamp-range
+                            (fedi--find-property-range 'timestamp
+                                                       previous-timestamp)))
+            (let* ((start (car next-timestamp-range))
+                   (end (cdr next-timestamp-range))
+                   (timestamp (get-text-property start 'timestamp))
+                   (current-display (get-text-property start 'display))
+                   (new-display (fedi--relative-time-description timestamp)))
+              (unless (string= current-display new-display)
+                (let ((inhibit-read-only t))
+                  (add-text-properties
+                   start end
+                   (list 'display
+                         (fedi--relative-time-description timestamp)))))
+              (fedi--consider-timestamp-for-updates timestamp)
+              (setq iteration (1+ iteration)
+                    previous-timestamp (1+ (cdr next-timestamp-range)))))
+          (if next-timestamp-range
+              ;; schedule the next batch from the previous location to
+              ;; start very soon in the future:
+              (run-at-time 0.1 nil #'fedi--update-timestamps-callback buffer
+                           (copy-marker previous-timestamp))
+            ;; otherwise we are done for now; schedule a new run for when 
needed
+            (setq fedi-timestamp-update-timer
+                  (run-at-time (time-to-seconds
+                                (time-subtract fedi-timestamp-next-update
+                                               (current-time)))
+                               nil ;; don't repeat
+                               #'fedi--update-timestamps-callback
+                               buffer nil))))))))
+
+(defun fedi--relative-time-details (timestamp &optional current-time)
+  "Return cons of (descriptive string . next change) for the TIMESTAMP.
+Use the optional CURRENT-TIME as the current time (only used for
+reliable testing).
+The descriptive string is a human readable version relative to
+the current time while the next change timestamp give the first
+time that this description will change in the future.
+TIMESTAMP is assumed to be in the past."
+  (let* ((now (or current-time (current-time)))
+         (time-difference (time-subtract now timestamp))
+         (seconds-difference (float-time time-difference))
+         (regular-response
+          (lambda (seconds-difference multiplier unit-name)
+            (let ((n (floor (+ 0.5 (/ seconds-difference multiplier)))))
+              (cons (format "%d %ss ago" n unit-name)
+                    (* (+ 0.5 n) multiplier)))))
+         (relative-result
+          (cond
+           ((< seconds-difference 60)
+            (cons "just now"
+                  60))
+           ((< seconds-difference (* 1.5 60))
+            (cons "1 minute ago"
+                  90)) ;; at 90 secs
+           ((< seconds-difference (* 60 59.5))
+            (funcall regular-response seconds-difference 60 "minute"))
+           ((< seconds-difference (* 1.5 60 60))
+            (cons "1 hour ago"
+                  (* 60 90))) ;; at 90 minutes
+           ((< seconds-difference (* 60 60 23.5))
+            (funcall regular-response seconds-difference (* 60 60) "hour"))
+           ((< seconds-difference (* 1.5 60 60 24))
+            (cons "1 day ago"
+                  (* 1.5 60 60 24))) ;; at a day and a half
+           ((< seconds-difference (* 60 60 24 6.5))
+            (funcall regular-response seconds-difference (* 60 60 24) "day"))
+           ((< seconds-difference (* 1.5 60 60 24 7))
+            (cons "1 week ago"
+                  (* 1.5 60 60 24 7))) ;; a week and a half
+           ((< seconds-difference (* 60 60 24 7 52))
+            (if (= 52 (floor (+ 0.5 (/ seconds-difference 60 60 24 7))))
+                (cons "52 weeks ago"
+                      (* 60 60 24 7 52))
+              (funcall regular-response seconds-difference (* 60 60 24 7) 
"week")))
+           ((< seconds-difference (* 1.5 60 60 24 365))
+            (cons "1 year ago"
+                  (* 60 60 24 365 1.5))) ;; a year and a half
+           (t
+            (funcall regular-response seconds-difference (* 60 60 24 365.25) 
"year")))))
+    (cons (car relative-result)
+          (time-add timestamp (seconds-to-time (cdr relative-result))))))
+
+(defun fedi--relative-time-description (timestamp &optional current-time)
+  "Return a string with a human readable TIMESTAMP relative to the current 
time.
+Use the optional CURRENT-TIME as the current time (only used for
+reliable testing).
+E.g. this could return something like \"1 min ago\", \"yesterday\", etc.
+TIME-STAMP is assumed to be in the past."
+  (car (fedi--relative-time-details timestamp current-time)))
+
 
 (provide 'fedi)
 ;;; fedi.el ends here

Reply via email to