From a0f8c83681086e122d79dc83fcf0ea97b2c3864e Mon Sep 17 00:00:00 2001
From: cage <cage-invalid@invalid>
Date: Sat, 9 Jan 2021 16:27:40 +0100
Subject: [PATCH] - [gemini] added a draft for gemlog subscriptions.

---
 etc/default-theme.conf              |  8 ++-
 etc/init.lisp                       | 12 ++++
 src/db.lisp                         | 91 ++++++++++++++++++++-------
 src/gemini-subscription-window.lisp | 97 +++++++++++++++++++++++++++++
 src/gemini/gemini-parser.lisp       | 35 +++++++++++
 src/gemini/package.lisp             |  7 +++
 src/gemini/subscription.lisp        |  3 +-
 src/keybindings.lisp                |  3 +
 src/package.lisp                    | 39 +++++++++++-
 src/program-events.lisp             | 70 +++++++++++++++++++--
 src/software-configuration.lisp     |  1 +
 src/specials.lisp                   |  3 +
 src/ui-goodies.lisp                 | 79 ++++++++++++++++++++---
 tinmop.asd                          |  1 +
 14 files changed, 408 insertions(+), 41 deletions(-)
 create mode 100644 src/gemini-subscription-window.lisp

diff --git a/etc/default-theme.conf b/etc/default-theme.conf
index cca399f..d17140f 100644
--- a/etc/default-theme.conf
+++ b/etc/default-theme.conf
@@ -518,4 +518,10 @@ chats-list-window.input.selected.foreground         = black
 
 # actual chat window
 
-chat-window.date-format.value                       = "[%hour:%min]"
\ No newline at end of file
+chat-window.date-format.value                       = "[%hour:%min]"
+
+# gemlog subscriptions
+
+gemini-subscription-window.background               = black
+
+gemini-subscription-window.foreground               = cyan
diff --git a/etc/init.lisp b/etc/init.lisp
index 84047b9..58b665b 100644
--- a/etc/init.lisp
+++ b/etc/init.lisp
@@ -145,6 +145,8 @@
 
 (define-key "M-c"       #'open-chats-list-window)
 
+(define-key "M-g s"     #'gemini-open-gemlog-window)
+
 ;; focus
 
 (define-key "f1"        #'focus-to-tags-window)
@@ -359,6 +361,16 @@
 
 (define-key "C-J"       #'gemini-delete-certificate                  *gemini-certificates-keymap*)
 
+;; gemini subscription window
+
+(define-key "C-J"       #'show-gemlog-to-screen                      *gemlog-subscription-keymap*)
+
+(define-key "up"        #'gemlogs-subscription-go-up                 *gemlog-subscription-keymap*)
+
+(define-key "down"      #'gemlogs-subscription-go-down               *gemlog-subscription-keymap*)
+
+(define-key "q"         #'close-gemlog-window                        *gemlog-subscription-keymap*)
+
 ;; tags keymap
 
 (define-key "up"        #'tag-go-up                                   *tags-keymap*)
diff --git a/src/db.lisp b/src/db.lisp
index be56aab..b1459a4 100644
--- a/src/db.lisp
+++ b/src/db.lisp
@@ -1751,6 +1751,10 @@ row."
 
 (gen-access-message-row title                       :title)
 
+(gen-access-message-row subtitle                    :subtitle)
+
+(gen-access-message-row url                         :url)
+
 (gen-access-message-row expire-date                 :expire-date)
 
 (gen-access-message-row chat-id                     :chat-id)
@@ -2872,6 +2876,35 @@ than `days-in-the-past' days (default: `(swconf:config-purge-cache-days-offset)'
               (row     (fetch-single query)))
     row))
 
+(defun row-unseen-count (row)
+  (and row
+       (db-getf row :unseen-count 0)))
+
+(defun row-seen-count (row)
+  (and row
+       (db-getf row :seen-count 0)))
+
+(defun gemini-all-subscriptions ()
+  (when-let* ((query (select (:gemini-subscription.*
+                              (:as (select (fields (:count :url))
+                                     (from :gemlog-entries)
+                                     (where (:and (:= :gemlog-entries.seenp
+                                                      (prepare-for-db nil :to-integer t))
+                                                  (:= :gemlog-entries.gemlog-id
+                                                   :gemini-subscription.url))))
+                               :unseen-count)
+                              (:as (select (fields (:count :url))
+                                     (from :gemlog-entries)
+                                     (where (:and (:= :gemlog-entries.seenp
+                                                      (prepare-for-db t :to-integer t))
+                                                  (:= :gemlog-entries.gemlog-id
+                                                   :gemini-subscription.url))))
+                               :seen-count))
+                       (from +table-gemini-subscription+)
+                       (order-by :title)))
+              (rows  (fetch-all-rows query)))
+    rows))
+
 (defun find-gemlog-entry (post-url)
   (when-let* ((query (select :*
                        (from +table-gemlog-entries+)
@@ -2888,7 +2921,7 @@ than `days-in-the-past' days (default: `(swconf:config-purge-cache-days-offset)'
                        :seenp)
                       (post-url
                        gemlog-iri
-                       post-date
+                       (decode-datetime-string post-date)
                        post-title
                        (prepare-for-db seenp :to-integer t)))))
 
@@ -2909,29 +2942,45 @@ than `days-in-the-past' days (default: `(swconf:config-purge-cache-days-offset)'
 
 (gen-access-message-row post-title      :post-title)
 
+(gen-access-message-row post-link       :post-link)
+
 (defun gemlog-entries (gemlog-url &key (unseen-only nil) (seen-only nil))
   (assert (not (and unseen-only
                     seen-only)))
-  (when-let* ((query (select ((:as :gemini-subscription.url      :gemlog-url)
-                              (:as :gemini-subscription.title    :gemlog-title)
-                              (:as :gemini-subscription.subtitle :gemlog-subtitle)
-                              (:as :gemlog-entries.date          :post-date)
-                              (:as :gemlog-entries.title         :post-title)
-                              (:as :gemlog-entries.seenp         :seenp))
-                       (from :gemlog-entries)
-                       (join :gemini-subscription
-                             :on (:= :gemlog-entries.gemlog-id
-                                  :gemini-subscription.url))
-                       (where (:= :gemini-subscription.url gemlog-url))
-                       (order-by (:desc :gemlog-entries.date))))
-              (rows    (fetch-all-rows query)))
-    (cond
-      (unseen-only
-       (remove-if-not (lambda (row) (db-nil-p (row-seenp row))) rows))
-      (seen-only
-       (remove-if (lambda (row) (db-nil-p (row-seenp row))) rows))
-      (t
-       rows))))
+  (when-let* ((query         (select ((:as :gemini-subscription.url      :gemlog-url)
+                                      (:as :gemini-subscription.title    :gemlog-title)
+                                      (:as :gemini-subscription.subtitle :gemlog-subtitle)
+                                      (:as :gemlog-entries.date          :post-date)
+                                      (:as :gemlog-entries.title         :post-title)
+                                      (:as :gemlog-entries.url           :post-link)
+                                      (:as :gemlog-entries.seenp         :seenp))
+                               (from :gemlog-entries)
+                               (join :gemini-subscription
+                                     :on (:= :gemlog-entries.gemlog-id
+                                          :gemini-subscription.url))
+                               (where (:= :gemini-subscription.url gemlog-url))))
+              (unordered-rows (fetch-all-rows query))
+              (actual-rows    (cond
+                                (unseen-only
+                                 (remove-if-not (lambda (row) (db-nil-p (row-seenp row)))
+                                                unordered-rows))
+                                (seen-only
+                                 (remove-if (lambda (row) (db-nil-p (row-seenp row)))
+                                            unordered-rows))
+                                (t
+                                 unordered-rows))))
+    (num:multisort actual-rows (list (num:gen-multisort-test string>
+                                                             string<
+                                                             (lambda (a)
+                                                               (row-post-date a)))
+                                     (num:gen-multisort-test (lambda (a b)
+                                                               (declare (ignore a))
+                                                               (db-nil-p b))
+                                                             (lambda (a b)
+                                                               (declare (ignore b))
+                                                               (db-nil-p a))
+                                                             (lambda (a)
+                                                               (db-getf a :seenp)))))))
 
 (defun delete-gemlog-entry (gemlog-url)
   (query (delete-from +table-gemlog-entries+ (where (:= :url gemlog-url)))))
diff --git a/src/gemini-subscription-window.lisp b/src/gemini-subscription-window.lisp
new file mode 100644
index 0000000..79e1b75
--- /dev/null
+++ b/src/gemini-subscription-window.lisp
@@ -0,0 +1,97 @@
+;; tinmop: an humble gemini and pleroma client
+;; Copyright (C) 2021  cage
+
+;; This program 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.
+
+;; This program 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 this program.
+;; If not, see [[http://www.gnu.org/licenses/][http://www.gnu.org/licenses/]].
+
+(in-package :gemini-subscription-window)
+
+(defclass gemini-subscription-window (focus-marked-window
+                                      simple-line-navigation-window
+                                      title-window
+                                      border-window)
+  ())
+
+(defmethod refresh-config :after ((object gemini-subscription-window))
+  (open-attach-window:refresh-view-links-window-config object
+                                                       swconf:+key-gemini-subscription-window+)
+  (refresh-config-sizes object swconf:+key-thread-window+)
+  (win-move object
+            (- (win-width *main-window*)
+               (win-width object))
+            0)
+  (win-move object
+            (- (win-width *main-window*)
+               (win-width object))
+            0)
+  object)
+
+(defun gemlog->text (gemlog-db-row window)
+  (format nil
+          "~a ~s ~a/~a"
+          (tui:text-ellipsis (db:row-title gemlog-db-row)
+                             (truncate (/ (win-width window)
+                                          3)))
+          (when (db:row-subtitle gemlog-db-row)
+            (tui:text-ellipsis (db:row-subtitle gemlog-db-row)
+                               (truncate (/ (win-width window)
+                                            3)))
+            (_ "No subtitle"))
+          (db:row-unseen-count gemlog-db-row)
+          (+ (db:row-unseen-count gemlog-db-row)
+             (db:row-seen-count   gemlog-db-row))))
+
+(defmethod resync-rows-db ((object gemini-subscription-window)
+                           &key
+                             (redraw t)
+                             (suggested-message-index nil))
+  (with-accessors ((rows             rows)
+                   (selected-line-bg selected-line-bg)
+                   (selected-line-fg selected-line-fg)) object
+    (flet ((make-rows (gemlogs bg fg)
+             (mapcar (lambda (gemlog)
+                       (make-instance 'line
+                                      :normal-text   (gemlog->text gemlog object)
+                                      :selected-text (gemlog->text gemlog object)
+                                      :fields        gemlog
+                                      :normal-bg     fg
+                                      :normal-fg     bg
+                                      :selected-bg   bg
+                                      :selected-fg   fg))
+                     gemlogs)))
+      (with-croatoan-window (croatoan-window object)
+        (setf rows (make-rows (db:gemini-all-subscriptions)
+                              selected-line-bg
+                              selected-line-fg))
+        (when suggested-message-index
+          (select-row object suggested-message-index))
+        (when redraw
+          (draw object))))))
+
+(defun open-gemini-subscription-window ()
+  (let* ((low-level-window (make-croatoan-window :enable-function-keys t)))
+    (setf *gemini-subscription-window*
+          (make-instance 'gemini-subscription-window
+                         :top-row-padding   0
+                         :title             (_ "Subscribed gemlogs")
+                         :single-row-height 1
+                         :uses-border-p     t
+                         :keybindings       keybindings:*gemlog-subscription-keymap*
+                         :croatoan-window   low-level-window))
+    (refresh-config  *gemini-subscription-window*)
+    (resync-rows-db  *gemini-subscription-window* :redraw nil)
+    (when (rows  *gemini-subscription-window*)
+      (select-row  *gemini-subscription-window* 0))
+    (draw  *gemini-subscription-window*)
+     *gemini-subscription-window*))
diff --git a/src/gemini/gemini-parser.lisp b/src/gemini/gemini-parser.lisp
index 4868df3..de20b1c 100644
--- a/src/gemini/gemini-parser.lisp
+++ b/src/gemini/gemini-parser.lisp
@@ -19,6 +19,41 @@
 
 (defparameter *raw-mode* nil)
 
+(define-constant +h1-prefix+           "#"   :test #'string=)
+
+(define-constant +h2-prefix+           "##"  :test #'string=)
+
+(define-constant +h3-prefix+           "###" :test #'string=)
+
+(define-constant +list-bullet-prefix+  "* "  :test #'string=)
+
+(define-constant +quote-prefix+        ">"   :test #'string=)
+
+(define-constant +preformatted-prefix+ "```" :test #'string=)
+
+(define-constant +link-prefix+         "=>"  :test #'string=)
+
+(defmacro gen-geminize-line (name prefix)
+  `(defun ,(format-fn-symbol t "geminize-~a" name) (text)
+     (strcat ,prefix text)))
+
+(gen-geminize-line h1 +h1-prefix+)
+
+(gen-geminize-line h2 +h2-prefix+)
+
+(gen-geminize-line h3 +h3-prefix+)
+
+(gen-geminize-line list +list-bullet-prefix+)
+
+(gen-geminize-line quote +quote-prefix+)
+
+(gen-geminize-line link +link-prefix+)
+
+(defun make-gemini-link (url title)
+  (format nil "~a ~a"
+          (geminize-link url)
+          title))
+
 (defrule space (or #\Space #\Tab)
   (:constant nil))
 
diff --git a/src/gemini/package.lisp b/src/gemini/package.lisp
index 6fa257e..9137851 100644
--- a/src/gemini/package.lisp
+++ b/src/gemini/package.lisp
@@ -37,6 +37,13 @@
   (:shadowing-import-from :misc :random-elt :shuffle)
   (:export
    :+gemini-scheme+
+   :geminize-h1
+   :geminize-h2
+   :geminize-h3
+   :geminize-list
+   :geminize-quote
+   :geminize-link
+   :make-gemini-link
    :gemini-link
    :target
    :name
diff --git a/src/gemini/subscription.lisp b/src/gemini/subscription.lisp
index e55ce6e..88ed6e0 100644
--- a/src/gemini/subscription.lisp
+++ b/src/gemini/subscription.lisp
@@ -46,7 +46,8 @@ A link text entry is like 'aaaa-mm-dd post title'
 
 This function parses the 'aaaa-mm-dd' part.
 "
-  (local-time:parse-timestring link-text :start 0 :end 10 :fail-on-error nil))
+  (when (>= (length link-text) 10)
+    (local-time:parse-timestring link-text :start 0 :end 10 :fail-on-error nil)))
 
 (defun link-post-title (link-text)
   "Returns the title of gemlog entry from link text
diff --git a/src/keybindings.lisp b/src/keybindings.lisp
index ced85ee..d1cbb7a 100644
--- a/src/keybindings.lisp
+++ b/src/keybindings.lisp
@@ -264,6 +264,9 @@ produces a tree and graft the latter on `existing-tree'"
 (defparameter *chat-message-keymap* (make-starting-comand-tree)
   "The keymap for message-window when shows chat.")
 
+(defparameter *gemlog-subscription-keymap*  (make-starting-comand-tree)
+  "The keymap for gemlog subscriptions window.")
+
 (defun define-key (key-sequence function &optional (existing-keymap *global-keymap*))
   "Define a key sequence that trigger a function:
 
diff --git a/src/package.lisp b/src/package.lisp
index 6cdcbfb..617c961 100644
--- a/src/package.lisp
+++ b/src/package.lisp
@@ -831,6 +831,8 @@
    :row-poll-expired-p
    :row-poll-multiple-vote-p
    :row-title
+   :row-subtitle
+   :row-url
    :row-expire-date
    :row-account-id
    :row-updated-at
@@ -951,14 +953,18 @@
    :find-tls-certificates-rows
    :gemini-subscribe-url
    :gemini-find-subscription
+   :gemini-all-subscriptions
+   :row-seen-count
+   :row-unseen-count
    :find-gemlog-entry
    :add-gemlog-entries
    :gemlog-mark-as-seen
    :gemlog-url
    :gemlog-title
    :gemlog-subtitle
-   :post-date
-   :post-title
+   :row-post-date
+   :row-post-title
+   :row-post-link
    :gemlog-entries
    :delete-gemlog-entry))
 
@@ -1022,6 +1028,7 @@
    :+key-message-window+
    :+key-chat-window+
    :+key-chats-list-window+
+   :+key-gemini-subscription-window+
    :+key-favourite+
    :+key-sensitive+
    :+key-boosted+
@@ -1216,6 +1223,7 @@
    :*open-message-link-window*
    :*gemini-streams-window*
    :*gemini-certificates-window*
+   :*gemini-subscription-window*
    :*chats-list-window*))
 
 (defpackage :complete
@@ -1340,6 +1348,7 @@
    :gemini-compact-lines-event
    :gemini-enqueue-download-event
    :gemini-gemlog-subscribe-event
+   :gemlog-show-event
    :get-chat-messages-event
    :get-chats-event
    :chat-show-event
@@ -1500,6 +1509,7 @@
    :*gemini-certificates-keymap*
    :*chats-list-keymap*
    :*chat-message-keymap*
+   :*gemlog-subscription-keymap*
    :define-key
    :init-keyboard-mapping
    :find-keymap-node
@@ -1957,6 +1967,26 @@
   (:export
    :open-gemini-certificates-window))
 
+(defpackage :gemini-subscription-window
+  (:use
+   :cl
+   :alexandria
+   :cl-ppcre
+   :access
+   :croatoan
+   :config
+   :constants
+   :text-utils
+   :misc
+   :mtree
+   :specials
+   :windows
+   :line-oriented-window
+   :tui-utils)
+  (:shadowing-import-from :misc :random-elt :shuffle)
+  (:export
+   :open-gemini-subscription-window))
+
 (defpackage :command-window
   (:use
    :cl
@@ -2353,6 +2383,11 @@
    :gemini-streams-window-open-stream
    :gemini-refresh-page
    :gemini-subscribe-gemlog
+   :gemini-open-gemlog-window
+   :gemlogs-subscription-go-up
+   :gemlogs-subscription-go-down
+   :close-gemlog-window
+   :show-gemlog-to-screen
    :send-to-pipe
    :send-message-to-pipe))
 
diff --git a/src/program-events.lisp b/src/program-events.lisp
index 6d03e09..028edc6 100644
--- a/src/program-events.lisp
+++ b/src/program-events.lisp
@@ -1143,14 +1143,72 @@
 (defmethod process-event ((object gemini-gemlog-subscribe-event))
   (with-accessors ((url payload)) object
     (let ((subscribedp (gemini-subscription:subscribe url)))
-      (when (not subscribedp)
-        (gemini-subscription:refresh url)
-        (ui:notify (format nil
-                           (_ "Unable to subscribe to ~s")
-                           url)
-                   :as-error t)))))
+      (if subscribedp
+          (gemini-subscription:refresh url)
+          (ui:notify (format nil
+                             (_ "Unable to subscribe to ~s")
+                             url)
+                     :as-error t)))))
 
 
+(defclass gemlog-show-event (program-event)
+  ((title
+    :initarg :title
+    :accessor title)
+   (subtitle
+    :initarg :subtitle
+    :accessor subtitle)
+   (gemlog-url
+    :initarg  :gemlog-url
+    :accessor gemlog-url)
+   (entries
+    :initarg :entries
+    :accessor entries)))
+
+(defmethod process-event ((object gemlog-show-event))
+  (with-accessors ((title      title)
+                   (subtitle   subtitle)
+                   (entries    entries)
+                   (gemlog-url gemlog-url)) object
+    (let* ((gemini-page (with-output-to-string (stream)
+                          (format stream
+                                  "~a~2%"
+                                  (gemini-parser:geminize-h1 title))
+                          (if subtitle
+                              (format stream
+                                      "~a~2%"
+                                      (gemini-parser:geminize-h2 subtitle))
+                              (format stream
+                                      "~a~2%"
+                                      (gemini-parser:geminize-h2 (_ "No subtitle"))))
+                          (loop for entry in entries do
+                            (let* ((link  (db:row-post-link  entry))
+                                   (date-format  (swconf:date-fmt swconf:+key-message-window+))
+                                   (date  (db:row-post-date entry))
+                                   (encoded-date (db-utils:encode-datetime-string date))
+                                   (title (text-utils:strcat (format-time encoded-date date-format)
+                                                             " "
+                                                             (db:row-post-title entry))))
+                              (format stream
+                                      "~a~%"
+                                      (gemini-parser:make-gemini-link link
+                                                                      title))))))
+           (url      (iri:iri-parse gemlog-url))
+           (parsed   (gemini-parser:parse-gemini-file gemini-page))
+           (links    (gemini-parser:sexp->links parsed
+                                                (uri:host url)
+                                                (uri:port url)
+                                                (uri:path url)))
+           (theme   gemini-client::*gemini-page-theme*))
+      (gemini-viewer::maybe-initialize-metadata specials:*message-window*)
+      (refresh-gemini-message-window links
+                                     gemini-page
+                                     (gemini-parser:sexp->text parsed theme)
+                                     nil)
+      (setf (windows:keybindings specials:*message-window*)
+            keybindings:*gemini-message-keymap*)
+      (windows:draw  specials:*message-window*))))
+
 ;;;; pleroma
 
 (defclass get-chat-messages-event (program-event)
diff --git a/src/software-configuration.lisp b/src/software-configuration.lisp
index c47a539..6383b2e 100644
--- a/src/software-configuration.lisp
+++ b/src/software-configuration.lisp
@@ -367,6 +367,7 @@
                    message-window
                    chat-window
                    chats-list-window
+                   gemini-subscription-window
                    attachment-header
                    max-numbers-allowed-attachments
                    max-message-length
diff --git a/src/specials.lisp b/src/specials.lisp
index 765c819..c357e28 100644
--- a/src/specials.lisp
+++ b/src/specials.lisp
@@ -58,5 +58,8 @@
 (defparameter *gemini-certificates-window*      nil
   "The window that shows all gemini client certificates.")
 
+(defparameter *gemini-subscription-window*      nil
+  "The window that shows all the subscribed gemlogs.")
+
 (defparameter *chats-list-window*               nil
   "The window that shows all the chats.")
diff --git a/src/ui-goodies.lisp b/src/ui-goodies.lisp
index 8c9f4e2..f471803 100644
--- a/src/ui-goodies.lisp
+++ b/src/ui-goodies.lisp
@@ -388,6 +388,8 @@ Metadata includes:
               (if print-message
                   (_ "focus passed on threads window")
                   nil)
+              *gemini-subscription-window*
+              *gemini-certificates-window*
               *chats-list-window*
               *gemini-streams-window*
               *open-message-link-window*
@@ -404,7 +406,8 @@ Metadata includes:
                      *message-window*
                      :documentation      "Move focus on message window"
                      :info-change-focus-message (_ "Focus passed on message window")
-                     :windows-lose-focus (*gemini-certificates-window*
+                     :windows-lose-focus (*gemini-subscription-window*
+                                          *gemini-certificates-window*
                                           *chats-list-window*
                                           *gemini-streams-window*
                                           *open-message-link-window*
@@ -419,7 +422,8 @@ Metadata includes:
                      *send-message-window*
                      :documentation      "Move focus on send message window"
                      :info-change-focus-message (_ "Focus passed on send message window")
-                     :windows-lose-focus (*gemini-certificates-window*
+                     :windows-lose-focus (*gemini-subscription-window*
+                                          *gemini-certificates-window*
                                           *chats-list-window*
                                           *gemini-streams-window*
                                           *open-message-link-window*
@@ -434,7 +438,8 @@ Metadata includes:
                      *follow-requests-window*
                      :documentation      "Move focus on follow requests window"
                      :info-change-focus-message (_ "Focus passed on follow requests window")
-                     :windows-lose-focus (*gemini-certificates-window*
+                     :windows-lose-focus (*gemini-subscription-window*
+                                          *gemini-certificates-window*
                                           *chats-list-window*
                                           *gemini-streams-window*
                                           *open-message-link-window*
@@ -449,7 +454,8 @@ Metadata includes:
                      *tags-window*
                      :documentation      "Move focus on tags window"
                      :info-change-focus-message (_ "Focus passed on tags window")
-                     :windows-lose-focus (*gemini-certificates-window*
+                     :windows-lose-focus (*gemini-subscription-window*
+                                          *gemini-certificates-window*
                                           *chats-list-window*
                                           *gemini-streams-window*
                                           *open-message-link-window*
@@ -463,7 +469,8 @@ Metadata includes:
                      *conversations-window*
                      :documentation      "Move focus on conversations window"
                      :info-change-focus-message (_ "Focus passed on conversation window")
-                     :windows-lose-focus (*gemini-certificates-window*
+                     :windows-lose-focus (*gemini-subscription-window*
+                                          *gemini-certificates-window*
                                           *chats-list-window*
                                           *gemini-streams-window*
                                           *open-message-link-window*
@@ -478,7 +485,8 @@ Metadata includes:
                      *open-attach-window*
                      :documentation      "Move focus on open-attach window"
                      :info-change-focus-message (_ "Focus passed on attach window")
-                     :windows-lose-focus (*gemini-certificates-window*
+                     :windows-lose-focus (*gemini-subscription-window*
+                                          *gemini-certificates-window*
                                           *chats-list-window*
                                           *gemini-streams-window*
                                           *open-message-link-window*
@@ -493,7 +501,8 @@ Metadata includes:
                      *open-message-link-window*
                      :documentation      "Move focus on open-link window"
                      :info-change-focus-message (_ "Focus passed on link window")
-                     :windows-lose-focus (*gemini-certificates-window*
+                     :windows-lose-focus (*gemini-subscription-window*
+                                          *gemini-certificates-window*
                                           *chats-list-window*
                                           *gemini-streams-window*
                                           *conversations-window*
@@ -508,7 +517,8 @@ Metadata includes:
                      *gemini-streams-window*
                      :documentation      "Move focus on open gemini streams window"
                      :info-change-focus-message (_ "Focus passed on gemini-stream window")
-                     :windows-lose-focus (*gemini-certificates-window*
+                     :windows-lose-focus (*gemini-subscription-window*
+                                          *gemini-certificates-window*
                                           *chats-list-window*
                                           *open-message-link-window*
                                           *conversations-window*
@@ -523,7 +533,8 @@ Metadata includes:
                      *chats-list-window*
                      :documentation      "Move focus on chats list window"
                      :info-change-focus-message (_ "Focus passed on chats list window")
-                     :windows-lose-focus (*gemini-certificates-window*
+                     :windows-lose-focus (*gemini-subscription-window*
+                                          *gemini-certificates-window*
                                           *gemini-streams-window*
                                           *open-message-link-window*
                                           *conversations-window*
@@ -538,7 +549,23 @@ Metadata includes:
                      *gemini-certificates-window*
                      :documentation      "Move focus on open-gemini certificates window"
                      :info-change-focus-message (_ "Focus passed on TLS certificates window.")
-                     :windows-lose-focus (*chats-list-window*
+                     :windows-lose-focus (*gemini-subscription-window*
+                                          *chats-list-window*
+                                          *gemini-streams-window*
+                                          *conversations-window*
+                                          *open-attach-window*
+                                          *tags-window*
+                                          *follow-requests-window*
+                                          *thread-window*
+                                          *message-window*
+                                          *send-message-window*))
+
+(gen-focus-to-window open-gemini-subscription-window
+                     *gemini-subscription-window*
+                     :documentation      "Move focus on open-gemini certificates window"
+                     :info-change-focus-message (_ "Focus passed on TLS certificates window.")
+                     :windows-lose-focus (*gemini-certificates-window*
+                                          *chats-list-window*
                                           *gemini-streams-window*
                                           *conversations-window*
                                           *open-attach-window*
@@ -1208,6 +1235,26 @@ certificate).
                       :prompt      (_ "Delete this certificate? [Y/n] ")
                       :complete-fn #'complete:complete-always-empty)))
 
+(defun gemini-open-gemlog-window ()
+  "Open a window with all the  gemlog subscribed."
+  (gemini-subscription-window:open-gemini-subscription-window)
+  (focus-to-open-gemini-subscription-window))
+
+(defun close-gemlog-window ()
+  (close-window-and-return-to-threads *gemini-subscription-window*))
+
+(defun show-gemlog-to-screen ()
+  (when-let* ((fields    (line-oriented-window:selected-row-fields *gemini-subscription-window*))
+              (gemlog-id (db:row-url fields))
+              (entries   (db:gemlog-entries gemlog-id))
+              (event     (make-instance 'program-events:gemlog-show-event
+                                        :gemlog-url gemlog-id
+                                        :title      (db:row-title fields)
+                                        :subtitle   (db:row-subtitle fields)
+                                        :entries    entries)))
+    (program-events:push-event event)
+    (focus-to-message-window)))
+
 (defun prompt-for-username (prompt complete-function event
                             notify-starting-message
                             notify-ending-message)
@@ -1783,6 +1830,18 @@ mot recent updated to least recent"
     (push-event event-abort)
     (push-event event-open)))
 
+(defun gemlogs-subscription-move (amount)
+  (ignore-errors
+    (line-oriented-window:unselect-all *gemini-subscription-window*)
+    (line-oriented-window:row-move     *gemini-subscription-window* amount)
+    (draw  *gemini-subscription-window*)))
+
+(defun gemlogs-subscription-go-down ()
+  (follow-request-move 1))
+
+(defun gemlogs-subscription-go-up ()
+  (follow-request-move -1))
+
 (defun gemini-subscribe-gemlog ()
   "Subscribe to the gemlog shown in the main window.
 
diff --git a/tinmop.asd b/tinmop.asd
index 55f0058..91d9c0c 100644
--- a/tinmop.asd
+++ b/tinmop.asd
@@ -114,6 +114,7 @@
                (:file "open-attach-window")
                (:file "open-message-link-window")
                (:file "gemini-client-certificates-window")
+               (:file "gemini-subscription-window")
                (:file "command-window")
                (:file "sending-message")
                (:file "follow-requests")