Private chats and guild channels are now being marked as unread if there are unread messages.
This feature doesn't work for edited messages nor is it 100% bulletproof as only the message IDs
are compared with each other due to API limitations. Servers won't be marked as red yet.
MODIFY
app/app.go
app/app.go
@@ -7,6 +7,7 @@ import (
"os"
"runtime"
+ "github.com/Bios-Marcel/cordless/readstate"
"github.com/Bios-Marcel/cordless/shortcuts"
"github.com/princebot/getpass"
@@ -52,6 +53,12 @@ func Run() {
app.SetRoot(splashScreen, true)
runNext := make(chan bool, 1)
+ go func() {
+ if loadError := readstate.Load(); loadError != nil {
+ panic(loadError)
+ }
+ }()
+
go func() {
configuration, configLoadError := config.LoadConfig()

ADD
readstate/readmarkers.go
+package readstate
+
+import (
+ "encoding/json"
+ "errors"
+ "io"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strconv"
+ "sync"
+
+ "github.com/Bios-Marcel/cordless/config"
+ "github.com/Bios-Marcel/discordgo"
+)
+
+var (
+ readMarkers = &ReadMarker{Data: make(map[string]uint64)}
+ readmarkersFilePath string
+ flushWaitGroup = &sync.WaitGroup{}
+)
+
+// ReadMarker contains the data that matches channel ids to their last read
+// message.
+type ReadMarker struct {
+ Data map[string]uint64
+}
+
+func init() {
+ configDir, configDirErr := config.GetConfigDirectory()
+ if configDirErr == nil {
+ readmarkersFilePath = filepath.Join(configDir, "readmarkers.json")
+ }
+
+ go func(waitGroup *sync.WaitGroup) {
+ for {
+ waitGroup.Add(75)
+ waitGroup.Wait()
+ Flush()
+ }
+ }(flushWaitGroup)
+}
+
+// Load loads the locally saved readmarkers returing an error if this failed.
+func Load() error {
+ if readmarkersFilePath == "" {
+ return errors.New("error loading data, filepath empty")
+ }
+
+ readmarkersFile, openError := os.Open(readmarkersFilePath)
+
+ if os.IsNotExist(openError) {
+ return nil
+ }
+
+ if openError != nil {
+ return openError
+ }
+
+ defer readmarkersFile.Close()
+ decoder := json.NewDecoder(readmarkersFile)
+ decodeError := decoder.Decode(readMarkers)
+
+ //io.EOF would mean empty, therefore we use defaults.
+ if decodeError != nil && decodeError != io.EOF {
+ return decodeError
+ }
+
+ return nil
+}
+
+// UpdateRead updates the local data for the passed channel using the passed
+// Message ID. It also calls Done on the waitgroup for flushing.
+func UpdateRead(channelID string, lastMessageID string) error {
+ parsed, parseError := strconv.ParseUint(lastMessageID, 10, 64)
+ if parseError != nil {
+ return parseError
+ }
+
+ readMarkers.Data[channelID] = parsed
+
+ flushWaitGroup.Done()
+
+ return nil
+}
+
+// HasBeenRead checks whether the passed channel has an unread Message or not.
+func HasBeenRead(channel *discordgo.Channel) bool {
+ if channel.LastMessageID == "" {
+ return true
+ }
+
+ data, present := readMarkers.Data[channel.ID]
+ if !present {
+ return false
+ }
+
+ parsed, parseError := strconv.ParseUint(channel.LastMessageID, 10, 64)
+ if parseError != nil {
+ return true
+ }
+
+ return data >= parsed
+}
+
+// Flush saves all local data to the harddrive.
+func Flush() error {
+ dataAsJSON, jsonError := json.MarshalIndent(readMarkers, "", " ")
+ if jsonError != nil {
+ return jsonError
+ }
+
+ return ioutil.WriteFile(readmarkersFilePath, dataAsJSON, 0666)
+}

MODIFY
ui/channeltree.go
ui/channeltree.go
@@ -5,6 +5,7 @@ import (
"github.com/Bios-Marcel/cordless/config"
"github.com/Bios-Marcel/cordless/discordutil"
+ "github.com/Bios-Marcel/cordless/readstate"
"github.com/Bios-Marcel/discordgo"
"github.com/Bios-Marcel/tview"
"github.com/gdamore/tcell"
@@ -81,6 +82,10 @@ func (channelTree *ChannelTree) LoadGuild(guildID string) error {
}
channelNode := createChannelNode(channel)
+ if !readstate.HasBeenRead(channel) {
+ channelTree.channelStates[channelNode] = channelUnread
+ channelNode.SetColor(tcell.ColorRed)
+ }
channelTree.internalTreeView.GetRoot().AddChild(channelNode)
}
@@ -101,17 +106,20 @@ func (channelTree *ChannelTree) LoadGuild(guildID string) error {
continue
}
- if channel.ParentID != "" {
channelNode := createChannelNode(channel)
for _, node := range channelTree.internalTreeView.GetRoot().GetChildren() {
channelID, ok := node.GetReference().(string)
if ok && channelID == channel.ParentID {
+ if !readstate.HasBeenRead(channel) {
+ channelTree.channelStates[channelNode] = channelUnread
+ channelNode.SetColor(tcell.ColorRed)
+ }
+
node.AddChild(channelNode)
break
}
}
}
- }
channelTree.internalTreeView.SetCurrentNode(channelTree.internalTreeView.GetRoot())

MODIFY
ui/privatechats.go
ui/privatechats.go
@@ -4,6 +4,7 @@ import (
"sort"
"github.com/Bios-Marcel/cordless/discordutil"
+ "github.com/Bios-Marcel/cordless/readstate"
"github.com/gdamore/tcell"
"github.com/Bios-Marcel/cordless/config"
@@ -110,7 +111,12 @@ func (privateList *PrivateChatList) prependChannel(channel *discordgo.Channel) {
}
func (privateList *PrivateChatList) addChannel(channel *discordgo.Channel) {
- privateList.chatsNode.AddChild(createPrivateChannelNode(channel))
+ newNode := createPrivateChannelNode(channel)
+ if !readstate.HasBeenRead(channel) {
+ privateList.privateChannelStates[newNode] = unread
+ newNode.SetColor(tcell.ColorRed)
+ }
+ privateList.chatsNode.AddChild(newNode)
}
func createPrivateChannelNode(channel *discordgo.Channel) *tview.TreeNode {

MODIFY
ui/window.go
ui/window.go
@@ -16,6 +16,7 @@ import (
"github.com/Bios-Marcel/cordless/config"
"github.com/Bios-Marcel/cordless/discordutil"
"github.com/Bios-Marcel/cordless/maths"
+ "github.com/Bios-Marcel/cordless/readstate"
"github.com/Bios-Marcel/cordless/scripting"
"github.com/Bios-Marcel/cordless/scripting/js"
"github.com/Bios-Marcel/cordless/shortcuts"
@@ -1137,6 +1138,7 @@ func (window *Window) startMessageHandlerRoutines(input, edit, delete chan *disc
window.session.State.MessageAdd(tempMessage)
if window.selectedChannel != nil && tempMessage.ChannelID == window.selectedChannel.ID {
+ readstate.UpdateRead(tempMessage.ChannelID, tempMessage.ID)
window.app.QueueUpdateDraw(func() {
window.chatView.AddMessage(tempMessage)
})
@@ -1441,6 +1443,9 @@ func (window *Window) SetCommandModeEnabled(enabled bool) {
func (window *Window) handleGlobalShortcuts(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyCtrlC {
+ if flushError := readstate.Flush(); flushError != nil {
+ panic(flushError)
+ }
window.doRestart <- false
return event
}
@@ -1729,6 +1734,8 @@ func (window *Window) LoadChannel(channel *discordgo.Channel) error {
window.app.SetFocus(window.messageInput.internalTextView)
}
+ readstate.UpdateRead(channel.ID, channel.LastMessageID)
+
return nil
}

Private chats and guild channels are now being marked as unread if there are unread messages.
This feature doesn't work for edited messages nor is it 100% bulletproof as only the message IDs
are compared with each other due to API limitations. Servers won't be marked as red yet.