Manage notification channels instead of an ntfy topic

Mirrors the server's channels API: add/remove channels per user from the Users
section. Requires the matching server release.

Claude-Session: https://claude.ai/code/session_016mBLURvJoMuUEr9cB2RpUN
This commit is contained in:
Niklas Ye
2026-10-12 21:00:40 +02:00
parent 77f2d6775b
commit ec5df0449b
10 changed files with 405 additions and 174 deletions
+41 -20
View File
@@ -48,7 +48,9 @@ const (
modeUserPicker
modeTeamPicker
modeUserCreate
modeUserNotifyEdit
modeChannelMenu
modeChannelAdd
modeChannelRemoveByID
modeAPIKeyMenu
modeAPIKeyCreate
modeAPIKeyReveal
@@ -356,7 +358,8 @@ type Model struct {
selectedUser api.User
userFormInputs [2]textinput.Model
userFormFocus int
ntfyTopicInput textinput.Model
channelAddInput textinput.Model
channelRemoveIn textinput.Model
apiKeyNameInput textinput.Model
apiKeyRevokeInput textinput.Model
revealedAPIKey api.APIKey
@@ -424,9 +427,13 @@ func NewModel(client *api.Client, serverURL string, refreshInterval time.Duratio
emailIn.Placeholder = "email"
emailIn.CharLimit = 128
topicIn := textinput.New()
topicIn.Placeholder = "ntfy topic — empty clears it"
topicIn.CharLimit = 128
channelIn := textinput.New()
channelIn.Placeholder = "type:address, e.g. ntfy:my-topic"
channelIn.CharLimit = 160
channelRemoveIn := textinput.New()
channelRemoveIn.Placeholder = "integer channel ID"
channelRemoveIn.CharLimit = 20
keyNameIn := textinput.New()
keyNameIn.Placeholder = "key name (e.g. laptop)"
@@ -458,7 +465,7 @@ func NewModel(client *api.Client, serverURL string, refreshInterval time.Duratio
}
for _, in := range []*textinput.Model{
&noteIn, &snoozeIn, &usernameIn, &emailIn, &topicIn, &keyNameIn, &revokeIn,
&noteIn, &snoozeIn, &usernameIn, &emailIn, &channelIn, &channelRemoveIn, &keyNameIn, &revokeIn,
} {
*in = st.Input(*in)
}
@@ -507,7 +514,8 @@ func NewModel(client *api.Client, serverURL string, refreshInterval time.Duratio
teamPickerTable: teamPickerT,
userManageTable: manageT,
userFormInputs: [2]textinput.Model{usernameIn, emailIn},
ntfyTopicInput: topicIn,
channelAddInput: channelIn,
channelRemoveIn: channelRemoveIn,
apiKeyNameInput: keyNameIn,
apiKeyRevokeInput: revokeIn,
pwInputs: pwIn,
@@ -668,11 +676,11 @@ func (m *Model) rebuildUserManageTable() {
m.userManageTable.SetColumns(userManageColumns(m.width))
rows := make([]table.Row, len(m.users))
for i, u := range m.users {
topic := u.Topic()
if topic == "" {
topic = "—"
channels := u.ChannelSummary()
if channels == "" {
channels = "—"
}
rows[i] = table.Row{u.Username, u.Email, topic, userFlags(u), u.CreatedAt.UTC().Format("2006-01-02")}
rows[i] = table.Row{u.Username, u.Email, channels, userFlags(u), u.CreatedAt.UTC().Format("2006-01-02")}
}
setRows(&m.userManageTable, rows)
m.userManageTable.SetHeight(tableHeight(m.height, 10))
@@ -768,7 +776,7 @@ func (m Model) canEditSchedule(t api.Team) bool {
}
// canManageUser mirrors the server's self-or-admin rule for a user's password,
// ntfy topic and API keys.
// notification channels and API keys.
func (m Model) canManageUser(u api.User) bool {
return m.isAdmin || u.ID == m.meID
}
@@ -940,17 +948,17 @@ func teamPickerColumns(width int) []table.Column {
func userManageColumns(width int) []table.Column {
createdW := 12
usernameW := 25
topicW := 22
channelsW := 22
flagsW := 14
// 10 = bubbles' Padding(0, 1) on each of the five cells.
emailW := width - usernameW - topicW - flagsW - createdW - 10
emailW := width - usernameW - channelsW - flagsW - createdW - 10
if emailW < 15 {
emailW = 15
}
return []table.Column{
{Title: "Username", Width: usernameW},
{Title: "Email", Width: emailW},
{Title: "Ntfy Topic", Width: topicW},
{Title: "Channels", Width: channelsW},
{Title: "Flags", Width: flagsW},
{Title: "Created", Width: createdW},
}
@@ -1496,12 +1504,25 @@ func createUserCmd(client *api.Client, username, email string) tea.Cmd {
}
}
// setUserNotifyTargetCmd points a user's pages at a topic, or clears it when
// topic is empty. It re-lists afterwards so the table shows what the server
// stored rather than what was typed.
func setUserNotifyTargetCmd(client *api.Client, userID int64, topic string) tea.Cmd {
// addChannelCmd gives a user a notification channel. It re-lists afterwards so
// the table shows what the server stored rather than what was typed.
func addChannelCmd(client *api.Client, userID int64, typ, address string) tea.Cmd {
return func() tea.Msg {
if _, err := client.SetUserNotifyTarget(userID, topic); err != nil {
if _, err := client.AddChannel(userID, typ, address); err != nil {
return userActionErrMsg{err}
}
users, err := client.ListUsers()
if err != nil {
return userActionErrMsg{err}
}
return usersFetchedMsg{users: users}
}
}
// removeChannelCmd deletes one of a user's channels and re-lists.
func removeChannelCmd(client *api.Client, userID, channelID int64) tea.Cmd {
return func() tea.Msg {
if err := client.RemoveChannel(userID, channelID); err != nil {
return userActionErrMsg{err}
}
users, err := client.ListUsers()