Files
org-social.el/ui/buffers/org-social-ui-timeline.el.orig
Andros Fenollosa 6a43cc51d5 New structure
2025-10-06 08:45:07 +02:00

531 lines
23 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.0
;; 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)
;; 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))
;; 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"
" 🔔 Notifications ")
(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 "\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 _) (org-social-ui--refresh))
:help-echo "Refresh timeline"
" ↻ Refresh ")
(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\n" nil "#666666")
(org-social-ui--insert-formatted-text "Actions: (N) Notifications | (G) Groups\n" nil "#666666")
(org-social-ui--insert-formatted-text "Other: (g) Refresh | (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 for pagination
(setq org-social-ui--timeline-current-list timeline)
;; Calculate pagination
(let* ((total-posts (length timeline))
(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-current-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-current-list
start-idx
(min end-idx (length org-social-ui--timeline-current-list)))))
(dolist (post posts-to-show)
(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"
" 🔔 Notifications ")
(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 "\n\n")
;; Action buttons
(widget-create 'push-button
:notify (lambda (&rest _) (org-social-ui--refresh))
:help-echo "Refresh notifications"
" ↻ Refresh ")
(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: (g) Refresh | (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://org-social-relay.andros.dev/\")"))
(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"))
(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)
(let ((buffer-name org-social-ui--timeline-buffer-name))
(switch-to-buffer 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)
;; Show loading message
(org-social-ui--insert-formatted-text "Loading timeline...\n" nil "#4a90e2")
;; 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
(progn
(message "Loading timeline from relay...")
(org-social-ui--load-timeline-from-relay))
;; Fallback to local feeds
(progn
(message "Loading timeline from 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)
(message "Debug: Checking existing feeds...")
(message "Debug: org-social-variables--feeds bound: %s" (boundp 'org-social-variables--feeds))
(when (boundp 'org-social-variables--feeds)
(message "Debug: org-social-variables--feeds length: %s"
(if org-social-variables--feeds (length org-social-variables--feeds) 0)))
;; Check if we already have feeds loaded
(if (and (boundp 'org-social-variables--feeds)
org-social-variables--feeds
(> (length org-social-variables--feeds) 0))
;; We have feeds, display them
(progn
(message "Debug: Using existing feeds...")
(let ((timeline (when (fboundp 'org-social-feed--get-timeline)
(org-social-feed--get-timeline))))
(message "Debug: Timeline length: %s" (if timeline (length timeline) 0))
(org-social-ui--check-replies-and-display-timeline timeline)))
;; No feeds loaded yet, start the loading process
(progn
(message "Debug: No feeds loaded, starting initialization...")
;; Load my profile first to get followers list
(when (fboundp 'org-social-file--read-my-profile)
(message "Debug: Reading my profile...")
(org-social-file--read-my-profile))
;; Check if we have relay configured
(message "Debug: Relay configured: %s"
(and (boundp 'org-social-relay) org-social-relay (not (string-empty-p org-social-relay))))
;; 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))
(progn
(message "Debug: Initializing feeds from relay...")
(org-social-feed--initialize-queue-from-relay))
(progn
(message "Debug: Initializing feeds from local followers...")
;; 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"))))))
(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-current-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-current-list
start-idx
(min end-idx (length org-social-ui--timeline-current-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."
(with-current-buffer org-social-ui--timeline-buffer-name
(let ((inhibit-read-only t)
(buffer-read-only nil))
(goto-char (point-max))
;; Remove loading message
(goto-char (point-min))
(when (search-forward "Loading timeline..." nil t)
(beginning-of-line)
(let ((line-start (point)))
(forward-line 1)
(delete-region line-start (point))))
;; 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)))))
(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))))
(org-social-ui--check-replies-and-display-timeline timeline)
(message "Timeline loaded with %d posts" (if timeline (length timeline) 0)))))
(defun org-social-ui--timeline-next-page ()
"Load and append next page of posts (infinite scroll)."
(interactive)
(when org-social-ui--timeline-current-list
(let* ((total-posts (length org-social-ui--timeline-current-list))
(posts-shown (* org-social-ui--current-page org-social-ui--posts-per-page)))
(when (< posts-shown total-posts)
(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))
(when (search-backward "Show more" nil t)
(beginning-of-line)
(let ((start (point)))
(forward-line 2)
(delete-region start (point))))
;; Insert new page of posts
(goto-char (point-max))
(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-current-list
start-idx
(min end-idx total-posts))))
(dolist (post posts-to-show)
(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 new "Show more" button if it exists
(goto-char (point-max))
(when (search-backward "Show more" nil t)
(forward-button 1))))))))))
(provide 'org-social-ui-timeline)
;;; org-social-ui-timeline.el ends here