mirror of
https://github.com/tanrax/org-social.el
synced 2026-01-09 14:23:33 +01:00
* Improvement: org-social-mention-user * Updated changelog * Improvement: `org-social-mention-user` - Now uses async cache system that loads all users from relay without blocking Emacs. Cache updates automatically and silently after timeline is displayed
601 lines
26 KiB
EmacsLisp
601 lines
26 KiB
EmacsLisp
;;; org-social-ui-timeline.el --- Timeline buffer for Org-social -*- lexical-binding: t -*- -*- coding: utf-8 -*-
|
|
|
|
;; SPDX-License-Identifier: GPL-3.0
|
|
;; Author: Andros Fenollosa <hi@andros.dev>
|
|
;; Version: 2.4
|
|
;; URL: https://github.com/tanrax/org-social.el
|
|
|
|
;;; Commentary:
|
|
;; Timeline view with pagination and auto-refresh.
|
|
|
|
;;; Code:
|
|
|
|
(require 'org-social-variables)
|
|
(require 'org-social-ui-core)
|
|
(require 'org-social-ui-utils)
|
|
(require 'org-social-ui-components)
|
|
|
|
;; Helper function to filter out reactions
|
|
(defun org-social-ui--filter-reactions (timeline)
|
|
"Filter out reactions and poll votes from TIMELINE.
|
|
Reactions are posts with reply_to and mood (regardless of text content).
|
|
Poll votes are posts with poll_option property."
|
|
(seq-filter
|
|
(lambda (post)
|
|
(let ((mood (alist-get 'mood post))
|
|
(reply-to (alist-get 'reply_to post))
|
|
(poll-option (alist-get 'poll_option post)))
|
|
;; Exclude reactions: posts with reply_to + mood (any text or empty)
|
|
;; Exclude poll votes: posts with poll_option
|
|
(not (or (and reply-to mood)
|
|
poll-option))))
|
|
timeline))
|
|
|
|
;; Helper function to filter out group posts (only for timeline)
|
|
(defun org-social-ui--filter-timeline-posts (timeline)
|
|
"Filter out reactions, poll votes, and group posts from TIMELINE.
|
|
This combines reaction filtering, poll vote filtering, and group filtering
|
|
specifically for the timeline view."
|
|
(seq-filter
|
|
(lambda (post)
|
|
(let ((mood (alist-get 'mood post))
|
|
(reply-to (alist-get 'reply_to post))
|
|
(group (alist-get 'group post))
|
|
(poll-option (alist-get 'poll_option post)))
|
|
;; Exclude reactions: posts with reply_to + mood (any text or empty)
|
|
;; Exclude poll votes: posts with poll_option
|
|
;; Exclude group posts: posts with group property
|
|
(not (or (and reply-to mood)
|
|
poll-option
|
|
group))))
|
|
timeline))
|
|
|
|
;; Forward declarations
|
|
(declare-function org-social-relay--get-timeline "org-social-relay" ())
|
|
(declare-function org-social-feed--get-timeline "org-social-feed" ())
|
|
(declare-function org-social-feed--process-queue "org-social-feed" ())
|
|
(declare-function org-social-relay--check-posts-for-replies "org-social-relay" (post-urls callback))
|
|
(declare-function org-social--format-date "org-social" (timestamp))
|
|
(declare-function org-social-file--new-post "org-social-file" (&optional reply-url reply-id))
|
|
(declare-function org-social-file--new-poll "org-social-file" ())
|
|
(declare-function org-social-ui-notifications "org-social-ui-notifications" ())
|
|
(declare-function org-social-ui-groups "org-social-ui-groups" ())
|
|
(declare-function org-social-ui-search "org-social-ui-search" ())
|
|
(declare-function org-social-ui-discover "org-social-ui-discover" ())
|
|
(declare-function org-social-ui-profile "org-social-ui-profile" (user-url))
|
|
|
|
;; Refresh timer variable
|
|
(defvar org-social-ui--refresh-timer nil
|
|
"Timer for automatic timeline refresh.")
|
|
|
|
(defun org-social-ui--insert-timeline-header ()
|
|
"Insert timeline header with navigation and actions."
|
|
(org-social-ui--insert-logo)
|
|
|
|
;; Navigation buttons
|
|
(widget-create 'push-button
|
|
:notify (lambda (&rest _) (org-social-ui-timeline))
|
|
:help-echo "View timeline"
|
|
" 📰 Timeline ")
|
|
|
|
(org-social-ui--insert-formatted-text " ")
|
|
|
|
(widget-create 'push-button
|
|
:notify (lambda (&rest _) (org-social-ui-notifications))
|
|
:help-echo "View notifications"
|
|
" 🔔 Notices ")
|
|
|
|
(org-social-ui--insert-formatted-text " ")
|
|
|
|
(widget-create 'push-button
|
|
:notify (lambda (&rest _) (org-social-ui-groups))
|
|
:help-echo "View groups"
|
|
" 👥 Groups ")
|
|
|
|
(org-social-ui--insert-formatted-text " ")
|
|
|
|
(widget-create 'push-button
|
|
:notify (lambda (&rest _) (org-social-ui-search))
|
|
:help-echo "Search posts"
|
|
" 🔍 Search ")
|
|
|
|
(org-social-ui--insert-formatted-text " ")
|
|
|
|
(widget-create 'push-button
|
|
:notify (lambda (&rest _) (org-social-ui-discover))
|
|
:help-echo "Discover users"
|
|
" 🌍 Discover ")
|
|
|
|
(org-social-ui--insert-formatted-text "\n\n")
|
|
|
|
;; Action buttons
|
|
(widget-create 'push-button
|
|
:notify (lambda (&rest _) (org-social-file--new-post))
|
|
:help-echo "Create a new post"
|
|
" + New Post ")
|
|
|
|
(org-social-ui--insert-formatted-text " ")
|
|
|
|
(widget-create 'push-button
|
|
:notify (lambda (&rest _) (org-social-file--new-poll))
|
|
:help-echo "Create a new poll"
|
|
" 📊 New Poll ")
|
|
|
|
(org-social-ui--insert-formatted-text " ")
|
|
|
|
(widget-create 'push-button
|
|
:notify (lambda (&rest _)
|
|
(when org-social-variables--my-profile
|
|
(let ((my-url (alist-get 'url org-social-variables--my-profile)))
|
|
(when my-url
|
|
(org-social-ui-profile my-url)))))
|
|
:help-echo "View your profile"
|
|
" 👤 Profile ")
|
|
|
|
(org-social-ui--insert-formatted-text " ")
|
|
|
|
(widget-create 'push-button
|
|
:notify (lambda (&rest _)
|
|
(browse-url "https://liberapay.com/org-social/"))
|
|
:help-echo "Support Org Social development"
|
|
" ❤ Donate ")
|
|
|
|
(org-social-ui--insert-formatted-text "\n\n")
|
|
|
|
;; Help text
|
|
(org-social-ui--insert-formatted-text "Navigation: (n) Next | (p) Previous | (t) Thread | (P) Profile\n" nil "#666666")
|
|
(org-social-ui--insert-formatted-text "Post: (c) New Post | (l) New Poll | (r) Reply | (R) React | (b) Boost\n" nil "#666666")
|
|
(org-social-ui--insert-formatted-text "Actions: (N) Notices | (G) Groups | (S) Search\n" nil "#666666")
|
|
(org-social-ui--insert-formatted-text "Other: (q) Quit\n" nil "#666666")
|
|
|
|
(org-social-ui--insert-separator))
|
|
|
|
(defun org-social-ui--insert-timeline-posts (timeline)
|
|
"Insert TIMELINE posts with infinite scroll pagination."
|
|
(when timeline
|
|
;; Store timeline globally (keep all data including reactions for detection)
|
|
(setq org-social-ui--timeline-current-list timeline)
|
|
;; Store filtered timeline for display (without reactions or group posts)
|
|
(setq org-social-ui--timeline-display-list (org-social-ui--filter-timeline-posts timeline))
|
|
|
|
(let* ((total-posts (length org-social-ui--timeline-display-list))
|
|
(posts-shown (* org-social-ui--current-page org-social-ui--posts-per-page)))
|
|
|
|
;; Insert posts for current page
|
|
(org-social-ui--insert-timeline-posts-paginated)
|
|
|
|
;; Insert "Show more" button if there are more posts
|
|
(when (< posts-shown total-posts)
|
|
(org-social-ui--insert-formatted-text "\n")
|
|
(widget-create 'push-button
|
|
:notify (lambda (&rest _) (org-social-ui--timeline-next-page))
|
|
:help-echo "Load more posts"
|
|
" Show more ")
|
|
(org-social-ui--insert-formatted-text "\n")))))
|
|
|
|
(defun org-social-ui--insert-timeline-posts-paginated ()
|
|
"Insert the current page of timeline posts."
|
|
(when org-social-ui--timeline-display-list
|
|
(let* ((start-idx (* (- org-social-ui--current-page 1) org-social-ui--posts-per-page))
|
|
(end-idx (* org-social-ui--current-page org-social-ui--posts-per-page))
|
|
(posts-to-show (cl-subseq org-social-ui--timeline-display-list
|
|
start-idx
|
|
(min end-idx (length org-social-ui--timeline-display-list)))))
|
|
(dolist (post posts-to-show)
|
|
;; Pass the full timeline (with reactions) for reaction detection
|
|
(org-social-ui--post-component post org-social-ui--timeline-current-list)))))
|
|
|
|
;;; Notifications Screen
|
|
|
|
(defun org-social-ui--insert-notifications-header ()
|
|
"Insert notifications header with navigation and actions."
|
|
(org-social-ui--insert-logo)
|
|
|
|
;; Navigation buttons
|
|
(widget-create 'push-button
|
|
:notify (lambda (&rest _) (org-social-ui-timeline))
|
|
:help-echo "View timeline"
|
|
" 📰 Timeline ")
|
|
|
|
(org-social-ui--insert-formatted-text " ")
|
|
|
|
(widget-create 'push-button
|
|
:notify (lambda (&rest _) (org-social-ui-notifications))
|
|
:help-echo "View notifications"
|
|
" 🔔 Notices ")
|
|
|
|
(org-social-ui--insert-formatted-text " ")
|
|
|
|
(widget-create 'push-button
|
|
:notify (lambda (&rest _) (org-social-ui-groups))
|
|
:help-echo "View groups"
|
|
" 👥 Groups ")
|
|
|
|
(org-social-ui--insert-formatted-text " ")
|
|
|
|
(widget-create 'push-button
|
|
:notify (lambda (&rest _) (org-social-ui-search))
|
|
:help-echo "Search posts"
|
|
" 🔍 Search ")
|
|
|
|
(org-social-ui--insert-formatted-text "\n\n")
|
|
|
|
;; Help text
|
|
(org-social-ui--insert-formatted-text "Your Mentions and Replies\n" 1.2 "#4a90e2")
|
|
(org-social-ui--insert-formatted-text "Navigation:\n" nil "#666666")
|
|
(org-social-ui--insert-formatted-text "(n) Next | (p) Previous | (T) Timeline | (G) Groups\n" nil "#666666")
|
|
(org-social-ui--insert-formatted-text "Other: (q) Quit\n" nil "#666666")
|
|
|
|
(org-social-ui--insert-separator))
|
|
|
|
(defun org-social-ui--mention-component (mention-url)
|
|
"Insert a mention component for MENTION-URL."
|
|
(org-social-ui--insert-formatted-text "📧 " 1.1 "#ff6600")
|
|
(org-social-ui--insert-formatted-text "New mention: ")
|
|
|
|
;; Extract info from URL (format: https://domain.com/social.org#timestamp)
|
|
(let ((author-url (when (string-match "\\(.*\\)#" mention-url)
|
|
(match-string 1 mention-url)))
|
|
(timestamp (when (string-match "#\\(.+\\)$" mention-url)
|
|
(match-string 1 mention-url))))
|
|
|
|
(when author-url
|
|
;; Author name button
|
|
(widget-create 'push-button
|
|
:notify `(lambda (&rest _)
|
|
(org-social-ui-profile ,author-url))
|
|
:help-echo (format "View profile: %s" author-url)
|
|
(format "@%s" (file-name-nondirectory (string-trim-right author-url "/social.org"))))
|
|
|
|
(org-social-ui--insert-formatted-text " • ")
|
|
|
|
(when timestamp
|
|
(org-social-ui--insert-formatted-text (org-social--format-date timestamp) nil "#666666")))
|
|
|
|
(org-social-ui--insert-formatted-text "\n ")
|
|
|
|
;; Action buttons
|
|
(widget-create 'push-button
|
|
:notify `(lambda (&rest _)
|
|
(org-social-ui-thread ,mention-url))
|
|
:help-echo "View thread"
|
|
" 🧵 View Thread ")
|
|
|
|
(org-social-ui--insert-formatted-text " ")
|
|
|
|
(when author-url
|
|
(widget-create 'push-button
|
|
:notify `(lambda (&rest _)
|
|
(org-social-file--new-post ,author-url ,timestamp))
|
|
:help-echo "Reply to mention"
|
|
" ↳ Reply "))
|
|
|
|
(org-social-ui--insert-formatted-text "\n")
|
|
(org-social-ui--insert-separator)))
|
|
|
|
(defun org-social-ui--insert-notifications-content (mentions)
|
|
"Insert notifications content with MENTIONS."
|
|
(if mentions
|
|
(progn
|
|
(org-social-ui--insert-formatted-text (format "Found %d mention%s:\n\n"
|
|
(length mentions)
|
|
(if (= (length mentions) 1) "" "s"))
|
|
nil "#4a90e2")
|
|
(dolist (mention mentions)
|
|
(org-social-ui--mention-component mention)))
|
|
(org-social-ui--insert-formatted-text "No new mentions or replies found.\n" nil "#666666")
|
|
(when (and (boundp 'org-social-relay) org-social-relay (not (string-empty-p org-social-relay)))
|
|
(org-social-ui--insert-formatted-text "Make sure your relay is properly configured.\n" nil "#666666"))
|
|
(org-social-ui--insert-formatted-text "\nYour public URL: " nil "#666666")
|
|
(org-social-ui--insert-formatted-text (or org-social-my-public-url "Not configured") nil "#4a90e2")))
|
|
|
|
;;; Main UI Functions
|
|
|
|
(defun org-social-ui-timeline ()
|
|
"Display timeline screen."
|
|
(interactive)
|
|
|
|
;; Validate required configuration
|
|
(unless (and (boundp 'org-social-file)
|
|
org-social-file
|
|
(not (string-empty-p org-social-file)))
|
|
(error "Org-social-file is not configured. Please set it in your configuration"))
|
|
|
|
(unless (and (boundp 'org-social-relay)
|
|
org-social-relay
|
|
(not (string-empty-p org-social-relay)))
|
|
(error "Org-social-relay is not configured. Please set it to a relay server URL (e.g., \"https://relay.org-social.org/\")"))
|
|
|
|
(unless (and (boundp 'org-social-my-public-url)
|
|
org-social-my-public-url
|
|
(not (string-empty-p org-social-my-public-url)))
|
|
(error "Org-social-my-public-url is not configured. Please set it to your public social.org URL"))
|
|
|
|
;; Validate social.org file before loading timeline
|
|
(when (and (boundp 'org-social-file)
|
|
(file-exists-p org-social-file)
|
|
(fboundp 'org-social-validator-validate-and-display))
|
|
(require 'org-social-validator)
|
|
(with-current-buffer (find-file-noselect org-social-file)
|
|
(org-social-validator-validate-and-display)))
|
|
|
|
(setq org-social-ui--current-screen 'timeline)
|
|
(setq org-social-ui--current-page 1)
|
|
|
|
;; Clear replies cache on timeline refresh
|
|
(clrhash org-social-ui--replies-cache)
|
|
|
|
;; Show message in minibuffer
|
|
(message "Building timeline...")
|
|
|
|
(let ((buffer-name org-social-ui--timeline-buffer-name))
|
|
;; Prepare buffer in background (don't switch yet)
|
|
(with-current-buffer (get-buffer-create buffer-name)
|
|
(kill-all-local-variables)
|
|
|
|
;; Disable read-only mode before modifying buffer
|
|
(setq buffer-read-only nil)
|
|
|
|
(let ((inhibit-read-only t))
|
|
(erase-buffer))
|
|
(remove-overlays)
|
|
|
|
;; Insert header
|
|
(org-social-ui--insert-timeline-header)
|
|
|
|
;; Set up the buffer with centering
|
|
(org-social-ui--setup-centered-buffer)
|
|
(goto-char (point-min)))
|
|
|
|
;; Load timeline data
|
|
(if (and (boundp 'org-social-relay)
|
|
org-social-relay
|
|
(not (string-empty-p org-social-relay)))
|
|
;; Use relay-first approach
|
|
(org-social-ui--load-timeline-from-relay)
|
|
;; Fallback to local feeds
|
|
(org-social-ui--load-timeline-from-feeds))))
|
|
|
|
(defun org-social-ui--load-timeline-from-relay ()
|
|
"Load timeline from relay server."
|
|
;; For now, fallback to feed method since we need to integrate with existing feed system
|
|
(org-social-ui--load-timeline-from-feeds))
|
|
|
|
(defun org-social-ui--load-timeline-from-feeds ()
|
|
"Load timeline from local feeds."
|
|
(condition-case err
|
|
(progn
|
|
;; Ensure we have required modules
|
|
(require 'org-social-feed)
|
|
(require 'org-social-file)
|
|
|
|
;; Clear cache to force fresh download
|
|
(setq org-social-variables--feeds nil)
|
|
(setq org-social-variables--queue nil)
|
|
(message "Cache cleared, loading fresh data from relay...")
|
|
|
|
;; Load my profile first to get followers list
|
|
(when (fboundp 'org-social-file--read-my-profile)
|
|
(org-social-file--read-my-profile))
|
|
|
|
;; Initialize feeds from relay if available, otherwise from local followers
|
|
(if (and (boundp 'org-social-relay)
|
|
org-social-relay
|
|
(not (string-empty-p org-social-relay))
|
|
(fboundp 'org-social-feed--initialize-queue-from-relay))
|
|
(org-social-feed--initialize-queue-from-relay)
|
|
;; Initialize queue from local followers
|
|
(when (fboundp 'org-social-feed--initialize-queue)
|
|
(org-social-feed--initialize-queue)
|
|
(org-social-feed--process-queue)))
|
|
|
|
;; Show message and set up timer to check for loaded feeds
|
|
(org-social-ui--setup-timeline-refresh-timer))
|
|
(error
|
|
(with-current-buffer org-social-ui--timeline-buffer-name
|
|
(let ((inhibit-read-only t))
|
|
(goto-char (point-max))
|
|
(org-social-ui--insert-formatted-text
|
|
(format "Error loading timeline: %s\n" (error-message-string err))
|
|
nil "#ff0000")))
|
|
;; Switch to buffer to show the error
|
|
(switch-to-buffer org-social-ui--timeline-buffer-name))))
|
|
|
|
(defun org-social-ui--check-replies-and-display-timeline (timeline)
|
|
"Check which posts have replies and then display TIMELINE.
|
|
Only checks posts that will be visible on the current page."
|
|
(if (and timeline
|
|
(> (length timeline) 0)
|
|
(boundp 'org-social-relay)
|
|
org-social-relay
|
|
(not (string-empty-p org-social-relay)))
|
|
(progn
|
|
;; Get only posts for current page
|
|
(let* ((start-idx (* (- org-social-ui--current-page 1) org-social-ui--posts-per-page))
|
|
(end-idx (* org-social-ui--current-page org-social-ui--posts-per-page))
|
|
(visible-posts (cl-subseq timeline
|
|
start-idx
|
|
(min end-idx (length timeline))))
|
|
(post-urls '()))
|
|
;; Extract post URLs only from visible posts
|
|
(dolist (post visible-posts)
|
|
(let* ((author-url (or (alist-get 'author-url post)
|
|
(alist-get 'url post)))
|
|
(timestamp (or (alist-get 'timestamp post)
|
|
(alist-get 'id post)))
|
|
(post-url (when (and author-url timestamp)
|
|
(if (string-empty-p author-url)
|
|
(format "%s#%s"
|
|
(alist-get 'url org-social-variables--my-profile)
|
|
timestamp)
|
|
(format "%s#%s" author-url timestamp)))))
|
|
(when post-url
|
|
(push post-url post-urls))))
|
|
;; Batch check for replies (only for visible posts)
|
|
(if post-urls
|
|
(org-social-relay--check-posts-for-replies
|
|
(nreverse post-urls)
|
|
(lambda (results)
|
|
;; Store results globally
|
|
(setq org-social-variables--posts-with-replies results)
|
|
;; Now display timeline
|
|
(org-social-ui--display-timeline timeline)))
|
|
;; No valid post URLs, just display timeline
|
|
(org-social-ui--display-timeline timeline))))
|
|
;; No timeline or relay not configured, just display
|
|
(org-social-ui--display-timeline timeline)))
|
|
|
|
(defun org-social-ui--check-replies-for-current-page (callback)
|
|
"Check replies for posts in current page and call CALLBACK when done."
|
|
(if (and (boundp 'org-social-relay)
|
|
org-social-relay
|
|
(not (string-empty-p org-social-relay))
|
|
org-social-ui--timeline-display-list)
|
|
(let* ((start-idx (* (- org-social-ui--current-page 1) org-social-ui--posts-per-page))
|
|
(end-idx (* org-social-ui--current-page org-social-ui--posts-per-page))
|
|
(visible-posts (cl-subseq org-social-ui--timeline-display-list
|
|
start-idx
|
|
(min end-idx (length org-social-ui--timeline-display-list))))
|
|
(post-urls '()))
|
|
;; Extract post URLs only from visible posts
|
|
(dolist (post visible-posts)
|
|
(let* ((author-url (or (alist-get 'author-url post)
|
|
(alist-get 'url post)))
|
|
(timestamp (or (alist-get 'timestamp post)
|
|
(alist-get 'id post)))
|
|
(post-url (when (and author-url timestamp)
|
|
(if (string-empty-p author-url)
|
|
(format "%s#%s"
|
|
(alist-get 'url org-social-variables--my-profile)
|
|
timestamp)
|
|
(format "%s#%s" author-url timestamp)))))
|
|
(when post-url
|
|
(push post-url post-urls))))
|
|
;; Check for replies (only for visible posts)
|
|
(if post-urls
|
|
(org-social-relay--check-posts-for-replies
|
|
(nreverse post-urls)
|
|
(lambda (results)
|
|
;; Merge with existing results
|
|
(setq org-social-variables--posts-with-replies
|
|
(append results org-social-variables--posts-with-replies))
|
|
(funcall callback)))
|
|
;; No posts to check, just continue
|
|
(funcall callback)))
|
|
;; No relay configured, just continue
|
|
(funcall callback)))
|
|
|
|
(defun org-social-ui--display-timeline (timeline)
|
|
"Display TIMELINE in the timeline buffer and switch to it."
|
|
(with-current-buffer org-social-ui--timeline-buffer-name
|
|
(let ((inhibit-read-only t)
|
|
(buffer-read-only nil))
|
|
;; Insert posts
|
|
(goto-char (point-max))
|
|
(if (and timeline (> (length timeline) 0))
|
|
(org-social-ui--insert-timeline-posts timeline)
|
|
(org-social-ui--insert-formatted-text
|
|
"No posts available. Check your relay configuration or followed users.\n"
|
|
nil "#888888"))
|
|
;; Enable read-only mode
|
|
(setq buffer-read-only t)
|
|
(goto-char (point-min))))
|
|
;; Now switch to the buffer (only after everything is ready)
|
|
(switch-to-buffer org-social-ui--timeline-buffer-name))
|
|
|
|
(defvar org-social-ui--refresh-timer nil
|
|
"Timer for refreshing timeline content.")
|
|
|
|
(defun org-social-ui--setup-timeline-refresh-timer ()
|
|
"Set up a timer to check for loaded feeds and refresh timeline."
|
|
(when org-social-ui--refresh-timer
|
|
(cancel-timer org-social-ui--refresh-timer))
|
|
(setq org-social-ui--refresh-timer
|
|
(run-with-timer 2 1 'org-social-ui--check-and-refresh-timeline)))
|
|
|
|
(defun org-social-ui--check-and-refresh-timeline ()
|
|
"Check if feeds are loaded and refresh timeline if they are."
|
|
(when (and (boundp 'org-social-variables--feeds)
|
|
org-social-variables--feeds
|
|
(> (length org-social-variables--feeds) 0))
|
|
;; Feeds are loaded, cancel timer and display timeline
|
|
(when org-social-ui--refresh-timer
|
|
(cancel-timer org-social-ui--refresh-timer)
|
|
(setq org-social-ui--refresh-timer nil))
|
|
(let ((timeline (when (fboundp 'org-social-feed--get-timeline)
|
|
(org-social-feed--get-timeline))))
|
|
;; Display timeline (reactions will be fetched automatically by post component)
|
|
(org-social-ui--check-replies-and-display-timeline timeline)
|
|
(message "Timeline ready with %d posts" (if timeline (length timeline) 0))
|
|
;; After timeline is displayed, update mentions cache in background
|
|
(when (fboundp 'org-social-file--update-mentions-cache-async)
|
|
(require 'org-social-file)
|
|
(org-social-file--update-mentions-cache-async)))))
|
|
|
|
(defun org-social-ui--timeline-next-page ()
|
|
"Load and append next page of posts (infinite scroll)."
|
|
(interactive)
|
|
(when (and org-social-ui--timeline-display-list
|
|
(not org-social-ui--timeline-loading-in-progress))
|
|
(let* ((total-posts (length org-social-ui--timeline-display-list))
|
|
(posts-shown (* org-social-ui--current-page org-social-ui--posts-per-page)))
|
|
(when (< posts-shown total-posts)
|
|
(setq org-social-ui--timeline-loading-in-progress t)
|
|
(setq org-social-ui--current-page (1+ org-social-ui--current-page))
|
|
;; Check replies for new page posts
|
|
(org-social-ui--check-replies-for-current-page
|
|
(lambda ()
|
|
;; Append new posts without clearing existing content
|
|
(let ((inhibit-read-only t)
|
|
(buffer-read-only nil))
|
|
(with-current-buffer org-social-ui--timeline-buffer-name
|
|
;; Find and remove the "Show more" button
|
|
(goto-char (point-max))
|
|
(let ((new-posts-start nil))
|
|
(when (search-backward "Show more" nil t)
|
|
(beginning-of-line)
|
|
;; Delete the newline before the button too
|
|
(when (and (not (bobp))
|
|
(eq (char-before) ?\n))
|
|
(backward-char))
|
|
(let ((start (point)))
|
|
(search-forward "Show more")
|
|
(forward-line 1)
|
|
(delete-region start (point))))
|
|
;; Save position where new posts will be inserted (current point after deletion)
|
|
(setq new-posts-start (point))
|
|
;; Insert new page of posts at current position
|
|
(let* ((start-idx (* (- org-social-ui--current-page 1) org-social-ui--posts-per-page))
|
|
(end-idx (* org-social-ui--current-page org-social-ui--posts-per-page))
|
|
(posts-to-show (cl-subseq org-social-ui--timeline-display-list
|
|
start-idx
|
|
(min end-idx total-posts))))
|
|
(dolist (post posts-to-show)
|
|
;; Pass full timeline for reaction detection
|
|
(org-social-ui--post-component post org-social-ui--timeline-current-list)))
|
|
;; Add new "Show more" button if there are more posts
|
|
(when (< (* org-social-ui--current-page org-social-ui--posts-per-page) total-posts)
|
|
(org-social-ui--insert-formatted-text "\n")
|
|
(widget-create 'push-button
|
|
:notify (lambda (&rest _) (org-social-ui--timeline-next-page))
|
|
:help-echo "Load more posts"
|
|
" Show more ")
|
|
(org-social-ui--insert-formatted-text "\n"))
|
|
(setq buffer-read-only t)
|
|
(widget-setup)
|
|
;; Move cursor to the first new post (after its separator)
|
|
(when new-posts-start
|
|
(goto-char new-posts-start)
|
|
;; Find the first separator of new posts
|
|
(let ((separator-regex (concat "^" (regexp-quote (org-social-ui--string-separator)) "$")))
|
|
(when (search-forward-regexp separator-regex nil t)
|
|
;; Move to the line after the separator (start of post content)
|
|
(forward-line 1)))
|
|
;; Execute "previous post" to position cursor correctly
|
|
(org-social-ui--goto-previous-post))
|
|
;; Clear loading flag
|
|
(setq org-social-ui--timeline-loading-in-progress nil))))))))))
|
|
|
|
(provide 'org-social-ui-timeline)
|
|
;;; org-social-ui-timeline.el ends here
|