Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ A terminal UI for browsing and posting to Discourse forums. It behaves like a li
- Reply to topics or specific posts (Markdown supported).
- Like/unlike posts.
- Search posts and jump directly to the matching topic context.
- View notifications in a dedicated list and jump straight to the related topic/post.
- Inline composer with cursor movement, line breaks, and a live character counter.
- Emoji replacements for common `:emoji:` tokens and `:)`-style smiles.
- YAML-driven themes (`default`, `slate`, `fairground`, `rust`) with per-color overrides.
Expand All @@ -31,6 +32,7 @@ A terminal UI for browsing and posting to Discourse forums. It behaves like a li
| Posting and replying | Full | New topics, topic replies, and post replies. |
| Likes | Full | Like/unlike from Topic View. |
| Search | Full | Search results open directly into matching topic/post context. |
| Notifications | Full | Dedicated notifications list with direct open into the related topic/post. |
| Theming | Full | Built-in themes plus YAML overrides. |
| Inline images | Full | `chafa` primary, `viu` fallback/override. |
| Live list update notification | Partial | Uses Discourse MessageBus channels and shows `New/updated (n)` in the topic-list status area. Current implementation tracks core list filters only; category/tag-scoped refinement is planned. |
Expand Down Expand Up @@ -192,14 +194,16 @@ Color translation:
- Use Up/Down arrows to navigate.
- Press Enter to open a topic.
- Press `1-0` to open the first 10 visible topics directly.
- Press `n` to create a new topic.
- Press `c` to create a new topic.
- Press `n` to open notifications.
- Press `s` to search.
- Press `f` to cycle the list filter (Latest, Unread, Private Messages, Hot, New, Top).
- Press `p` to change Top period (daily, weekly, monthly, quarterly, yearly).
- Press `g` to refresh.
- Press `q` to quit.

The status bar shows the current list filter and your logged-in username.
If you have unread notifications, an accent badge like `[3]` appears beside the username.

Private Messages list view:
- Uses PM-specific columns in wide layouts.
Expand All @@ -222,6 +226,7 @@ Private Messages list view:
- `r` reply to the topic.
- `p` reply to the selected post.
- `s` search from within a topic.
- `n` opens notifications.
- `x` toggle fullscreen image view when the selected post shows an image preview.
- `esc` goes back to the list.
- `q` quits.
Expand All @@ -232,10 +237,18 @@ In fullscreen image view, press `x` or `esc` to return to the topic.
### Search
- Press `s` to open search.
- Type your query; Enter runs the search.
- Press `n` to open notifications.
- Arrow keys move through results; Enter opens the topic at the matching post.
- From a search-opened topic, `esc` returns to search results.
- From search results, `esc` returns to the topic list.

### Notifications
- Press `n` from the topic list, topic view, or search results to open notifications.
- Arrow keys move through notifications; Enter opens the related topic/post.
- Press `f` to cycle notification filters (`All`, `Responses`, `Likes`, `Mentions`, `Edits`, `Links`, `Messages`).
- Opening a notification marks it read in termcourse.
- `esc` returns to the previous screen.

## Debug & Logging

- HTTP debug logs are **opt-in**: set `TERMCOURSE_HTTP_DEBUG=1`.
Expand Down
21 changes: 21 additions & 0 deletions lib/termcourse/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,20 @@ def search(query)
get_json("/search.json", q: query)
end

def notifications(offset: 0, limit: 60, filter: nil)
params = { offset: offset.to_i, limit: limit.to_i }
params[:filter] = filter if filter
get_json("/notifications.json", params)
end

def mark_notification_read(notification_id)
put_json("/notifications/mark-read", id: notification_id.to_i)
end

def notification_totals
get_json("/notifications/totals.json")
end

def get_bytes(path_or_url, max_bytes: nil, redirect_limit: 4)
response = perform_request(:get, path_or_url, nil)
if response.status >= 300 && response.status < 400
Expand Down Expand Up @@ -206,6 +220,11 @@ def delete_json(path, params = nil)
parse_json(response.body)
end

def put_json(path, payload)
response = perform_request(:put, path, payload)
parse_json(response.body)
end

def parse_json(body)
return {} if body.nil? || body.strip.empty?

Expand Down Expand Up @@ -247,6 +266,8 @@ def perform_request(method, path_or_url, payload = nil, use_ipv4: false, allow_i
connection.get(path_or_url, payload, headers)
when :post
connection.post(path_or_url, payload, headers)
when :put
connection.put(path_or_url, payload, headers)
when :delete
connection.delete(path_or_url, payload, headers)
else
Expand Down
34 changes: 34 additions & 0 deletions lib/termcourse/live_updates.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ def initialize(base_url, headers:, current_user_id: nil, client: nil, debug: nil
@filter = :latest
@incoming_topic_ids = Set.new
@incoming_topic_order = []
@unread_notification_count = nil

subscribe_channels
end
Expand Down Expand Up @@ -49,6 +50,16 @@ def has_incoming?
incoming_count.positive?
end

def unread_notification_count
@mutex.synchronize { @unread_notification_count }
end

def set_unread_notification_count(count)
@mutex.synchronize do
@unread_notification_count = [count.to_i, 0].max
end
end

private

def subscribe_channels
Expand All @@ -58,6 +69,7 @@ def subscribe_channels
subscribe("/new")
subscribe("/unread")
subscribe("/unread/#{@current_user_id}")
subscribe("/notification/#{@current_user_id}")
end

def subscribe(channel)
Expand All @@ -70,6 +82,10 @@ def subscribe(channel)

def handle_message(channel, data)
payload = data.is_a?(Hash) ? data : {}
if notification_channel?(channel)
update_unread_notification_count(payload)
return
end
return unless count_message?(channel, payload)

topic_id = payload["topic_id"].to_i
Expand Down Expand Up @@ -103,10 +119,28 @@ def unread_channel?(channel)
channel == "/unread" || channel == "/unread/#{@current_user_id}"
end

def notification_channel?(channel)
channel == "/notification/#{@current_user_id}"
end

def private_message?(data)
data.dig("payload", "archetype").to_s == "private_message"
end

def update_unread_notification_count(data)
previous_unread = @mutex.synchronize { @unread_notification_count }
count =
if data.key?("all_unread_notifications_count") && data.key?("new_personal_messages_notifications_count")
data["all_unread_notifications_count"].to_i - data["new_personal_messages_notifications_count"].to_i
elsif data.key?("unread_notifications")
data["unread_notifications"].to_i
else
previous_unread
end
@mutex.synchronize { @unread_notification_count = [count, 0].max }
debug_log("live_updates_notifications unread=#{@unread_notification_count}")
end

def clear_incoming!
@incoming_topic_ids.clear
@incoming_topic_order.clear
Expand Down
Loading
Loading