159 lines
5.0 KiB
Ruby
159 lines
5.0 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module UI
|
|
module Windows
|
|
module_function
|
|
|
|
# Two-pane message reader: left=board list, right=messages of the selected
|
|
# board. Press "n" or click [+ New] to compose, "r" to refresh.
|
|
def messages(app, services:)
|
|
cols = app.cols
|
|
rows = app.rows
|
|
w = cols - 4
|
|
h = rows - 6
|
|
window = BBS::Window.new(title: ' Message Boards ')
|
|
window.layout(3, 3, w, h)
|
|
window.theme = app.theme
|
|
|
|
boards = services[:boards].boards
|
|
board_list = BBS::Widgets::ListBox.new(
|
|
items: boards,
|
|
label: ->(b) { " #{b.title}".ljust(18) }
|
|
)
|
|
board_list.layout(window.bounds.x + 2, window.bounds.y + 2, 20, h - 5)
|
|
window.add(board_list)
|
|
|
|
msg_lines = BBS::Widgets::ScrollView.new(lines: [])
|
|
msg_lines.layout(window.bounds.x + 23, window.bounds.y + 2, w - 26, h - 5)
|
|
window.add(msg_lines)
|
|
|
|
reload = lambda do
|
|
board = board_list.selected_item
|
|
next unless board
|
|
msgs = services[:boards].messages(board.id, 100)
|
|
wrap_width = msg_lines.bounds.width - 1
|
|
lines = []
|
|
if msgs.empty?
|
|
lines << " (no messages on #{board.title} yet — press 'n' to start one)"
|
|
else
|
|
msgs.each do |m|
|
|
head = "\e[2;37m#{m.timestamp}\e[0m \e[1;33m#{m.username}\e[0m: "
|
|
body = m.text.to_s
|
|
wrap_text(head + body, wrap_width).each { |l| lines << l }
|
|
lines << ''
|
|
end
|
|
end
|
|
msg_lines.lines = lines
|
|
msg_lines.scroll = [lines.size - msg_lines.bounds.height, 0].max
|
|
end
|
|
|
|
board_list.on_select = ->(_, _) { reload.call }
|
|
|
|
compose = BBS::Widgets::Button.new(
|
|
label: '&New',
|
|
on_click: ->(_) { compose_message(app, services: services, board: board_list.selected_item, on_posted: reload) }
|
|
)
|
|
compose.layout(window.bounds.x + 2, window.bounds.y + h - 2, 9, 1)
|
|
window.add(compose)
|
|
|
|
refresh = BBS::Widgets::Button.new(
|
|
label: '&Refresh',
|
|
on_click: ->(_) { reload.call }
|
|
)
|
|
refresh.layout(window.bounds.x + 13, window.bounds.y + h - 2, 13, 1)
|
|
window.add(refresh)
|
|
|
|
close = BBS::Widgets::Button.new(
|
|
label: '&Close',
|
|
on_click: ->(_) { app.close_window(window) }
|
|
)
|
|
close.layout(window.bounds.x + w - 11, window.bounds.y + h - 2, 9, 1)
|
|
window.add(close)
|
|
|
|
reload.call
|
|
window.reset_focus
|
|
window.focus_manager.focus(board_list)
|
|
app.open_window(window)
|
|
window
|
|
end
|
|
|
|
def compose_message(app, services:, board:, on_posted: nil)
|
|
return BBS::Dialogs.message(app, 'Select a board first.', title: ' Compose ') unless board
|
|
|
|
w = [app.cols - 8, 70].min
|
|
h = 14
|
|
window = BBS::Dialogs.centred_window(app, width: w, height: h,
|
|
title: " Post to #{board.title} ")
|
|
|
|
hint = BBS::Widgets::Label.new(
|
|
text: "From: #{app.context[:username] || 'Anonymous'} — Ctrl-Enter sends",
|
|
style_key: :input_label
|
|
)
|
|
hint.layout(window.bounds.x + 2, window.bounds.y + 1, w - 4, 1)
|
|
window.add(hint)
|
|
|
|
area = BBS::Widgets::TextArea.new(max_length: 1000)
|
|
area.layout(window.bounds.x + 2, window.bounds.y + 3, w - 4, h - 6)
|
|
window.add(area)
|
|
|
|
status = BBS::Widgets::Label.new(text: '', style_key: :error)
|
|
status.layout(window.bounds.x + 2, window.bounds.y + h - 3, w - 4, 1)
|
|
window.add(status)
|
|
|
|
send_btn = BBS::Widgets::Button.new(
|
|
label: '&Send',
|
|
on_click: lambda do |_|
|
|
result = services[:boards].post(board.id, app.context[:username] || 'Anonymous', area.value)
|
|
if result == :empty
|
|
status.text = 'Message cannot be empty.'
|
|
else
|
|
app.close_window(window)
|
|
on_posted&.call
|
|
end
|
|
end
|
|
)
|
|
send_btn.layout(window.bounds.x + w - 22, window.bounds.y + h - 2, 9, 1)
|
|
window.add(send_btn)
|
|
|
|
cancel = BBS::Widgets::Button.new(
|
|
label: '&Cancel',
|
|
on_click: ->(_) { app.close_window(window) }
|
|
)
|
|
cancel.layout(window.bounds.x + w - 12, window.bounds.y + h - 2, 10, 1)
|
|
window.add(cancel)
|
|
|
|
window.reset_focus
|
|
window.focus_manager.focus(area)
|
|
app.open_window(window)
|
|
window
|
|
end
|
|
|
|
def wrap_text(text, width)
|
|
return [''] if width <= 1
|
|
# Treat the text as containing ANSI; wrap on visible width approximately
|
|
# by splitting on whitespace.
|
|
visible = text.gsub(/\e\[[\d;]*m/, '')
|
|
return [text] if visible.length <= width
|
|
lines = []
|
|
cur = +''
|
|
cur_visible = +''
|
|
text.scan(/(\e\[[\d;]*m|\S+\s*|\s+)/) do |(token)|
|
|
if token.start_with?("\e")
|
|
cur << token
|
|
next
|
|
end
|
|
visible_tok = token
|
|
if cur_visible.length + visible_tok.length > width && !cur_visible.empty?
|
|
lines << cur
|
|
cur = +''
|
|
cur_visible = +''
|
|
end
|
|
cur << token
|
|
cur_visible << token
|
|
end
|
|
lines << cur unless cur.empty?
|
|
lines
|
|
end
|
|
end
|
|
end
|