Make news stashable

This commit is contained in:
Christian Rocha 2020-12-09 20:12:01 -05:00
parent 4fcf48f92a
commit 88806c8abc
5 changed files with 126 additions and 54 deletions

1
go.mod
View file

@ -20,6 +20,7 @@ require (
github.com/muesli/reflow v0.2.0
github.com/muesli/termenv v0.7.4
github.com/sahilm/fuzzy v0.1.0
github.com/segmentio/ksuid v1.0.3
github.com/spf13/cobra v1.1.1
github.com/spf13/viper v1.7.0
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897

4
go.sum
View file

@ -241,7 +241,11 @@ github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94 h1:G04eS0JkA
github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94/go.mod h1:b18R55ulyQ/h3RaWyloPyER7fWQVZvimKKhnI5OfrJQ=
github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/segmentio/ksuid v1.0.3 h1:FoResxvleQwYiPAVKe1tMUlEirodZqlqglIuFsdDntY=
github.com/segmentio/ksuid v1.0.3/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=

View file

@ -9,6 +9,7 @@ import (
"github.com/charmbracelet/charm"
"github.com/dustin/go-humanize"
"github.com/segmentio/ksuid"
"golang.org/x/text/runes"
"golang.org/x/text/transform"
"golang.org/x/text/unicode/norm"
@ -18,6 +19,11 @@ import (
type markdown struct {
markdownType DocType
// Local identifier. This allows us to precisely determine the stashed
// state of a markdown, regardless of whether it exists locally or on the
// network.
localID ksuid.KSUID
// Full path of a local markdown file. Only relevant to local documents and
// those that have been stashed in this session.
localPath string
@ -30,6 +36,12 @@ type markdown struct {
charm.Markdown
}
func (m *markdown) generateLocalID() {
if m.localID.IsNil() {
m.localID = ksuid.New()
}
}
// Generate the value we're doing to filter against.
func (m *markdown) buildFilterValue() {
note, err := normalize(m.Note)

View file

@ -18,6 +18,7 @@ import (
"github.com/muesli/reflow/ansi"
te "github.com/muesli/termenv"
"github.com/sahilm/fuzzy"
"github.com/segmentio/ksuid"
)
const (
@ -37,9 +38,15 @@ var (
// MSG
type fetchedMarkdownMsg *markdown
type deletedStashedItemMsg int
type filteredMarkdownMsg []*markdown
type fetchedMarkdownMsg *markdown
type markdownFetchFailedMsg struct {
err error
id int
note string
}
// MODEL
@ -138,7 +145,7 @@ type stashModel struct {
// Paths to files stashed this session. We treat this like a set, ignoring
// the value portion with an empty struct.
filesStashed map[string]struct{}
filesStashed map[ksuid.KSUID]struct{}
// Page we're fetching stash items from on the server, which is different
// from the local pagination. Generally, the server will return more items
@ -268,6 +275,10 @@ func (m stashModel) selectedMarkdown() *markdown {
// Adds markdown documents to the model.
func (m *stashModel) addMarkdowns(mds ...*markdown) {
if len(mds) > 0 {
for _, md := range mds {
md.generateLocalID()
}
m.markdowns = append(m.markdowns, mds...)
if !m.isFiltering() {
sort.Stable(markdownsByLocalFirst(m.markdowns))
@ -340,7 +351,7 @@ func (m *stashModel) openMarkdown(md *markdown) tea.Cmd {
if md.markdownType == LocalDoc {
cmd = loadLocalMarkdown(md)
} else {
cmd = loadRemoteMarkdown(m.general.cc, md.ID, md.markdownType)
cmd = loadRemoteMarkdown(m.general.cc, md)
}
return tea.Batch(cmd, spinner.Tick)
@ -462,7 +473,7 @@ func newStashModel(general *general) stashModel {
serverPage: 1,
loaded: NewDocTypeSet(),
loadingFromNetwork: true,
filesStashed: make(map[string]struct{}),
filesStashed: make(map[ksuid.KSUID]struct{}),
sections: s,
}
@ -533,6 +544,14 @@ func (m stashModel) update(msg tea.Msg) (stashModel, tea.Cmd) {
m.addMarkdowns(docs...)
case markdownFetchFailedMsg:
s := "Couldn't load markdown"
if msg.note != "" {
s += ": " + msg.note
}
cmd := m.newStatusMessage(s)
return m, cmd
case filteredMarkdownMsg:
m.filteredMarkdowns = msg
return m, nil
@ -714,23 +733,20 @@ func (m *stashModel) handleDocumentBrowsing(msg tea.Msg) tea.Cmd {
md := m.selectedMarkdown()
if _, alreadyStashed := m.filesStashed[md.localPath]; alreadyStashed {
if _, alreadyStashed := m.filesStashed[md.localID]; alreadyStashed {
cmds = append(cmds, m.newStatusMessage("Already stashed"))
break
}
isLocalMarkdown := md.markdownType == LocalDoc
markdownPathMissing := md.localPath == ""
if !isLocalMarkdown || markdownPathMissing {
if debug && isLocalMarkdown && markdownPathMissing {
log.Printf("refusing to stash markdown; local path is empty: %#v", md)
if !stashableDocTypes.Contains(md.markdownType) || md.localID.IsNil() {
if debug && md.localID.IsNil() {
log.Printf("refusing to stash markdown; local ID path is nil: %#v", md)
}
break
}
// Checks passed; perform the stash
m.filesStashed[md.localPath] = struct{}{}
m.filesStashed[md.localID] = struct{}{}
cmds = append(cmds, stashDocument(m.general.cc, *md))
if m.loadingDone() && !m.spinner.Visible() {
@ -807,7 +823,6 @@ func (m *stashModel) handleDocumentBrowsing(msg tea.Msg) tea.Cmd {
func (m *stashModel) handleDeleteConfirmation(msg tea.Msg) tea.Cmd {
if msg, ok := msg.(tea.KeyMsg); ok {
switch msg.String() {
// Confirm deletion
case "y":
if m.selectionState != selectionPromptingDelete {
break
@ -819,10 +834,8 @@ func (m *stashModel) handleDeleteConfirmation(msg tea.Msg) tea.Cmd {
continue
}
if md.markdownType == ConvertedDoc {
// Remove from the things-we-stashed-this-session set
delete(m.filesStashed, md.localPath)
}
// Remove from the things-we-stashed-this-session set
delete(m.filesStashed, md.localID)
// Delete optimistically and remove the stashed item before
// we've received a success response.
@ -976,7 +989,8 @@ func (m stashModel) view() string {
// Rules for the logo, filter and status message.
var logoOrFilter string
if m.showStatusMessage {
logoOrFilter = greenFg(m.statusMessage)
const gutter = 3
logoOrFilter = greenFg(truncate(m.statusMessage, m.general.width-gutter))
} else if m.isFiltering() {
logoOrFilter = m.filterInput.View()
} else {
@ -1184,33 +1198,49 @@ func (m stashModel) populatedView() string {
// COMMANDS
func loadRemoteMarkdown(cc *charm.Client, id int, t DocType) tea.Cmd {
// loadRemoteMarkdown is a command for loading markdown from the server.
func loadRemoteMarkdown(cc *charm.Client, md *markdown) tea.Cmd {
return func() tea.Msg {
var (
md *charm.Markdown
err error
)
if t == StashedDoc || t == ConvertedDoc {
md, err = cc.GetStashMarkdown(id)
} else {
md, err = cc.GetNewsMarkdown(id)
}
md, err := loadMarkdownFromCharm(cc, md.ID, md.markdownType)
if err != nil {
if debug {
log.Println("error loading remote markdown:", err)
log.Printf("error loading %s markdown (ID %d, Note: '%s'): %v", md.markdownType, md.ID, md.Note, err)
}
return markdownFetchFailedMsg{
err: err,
id: md.ID,
note: md.Note,
}
return errMsg{err}
}
return fetchedMarkdownMsg(&markdown{
markdownType: t,
Markdown: *md,
})
return fetchedMarkdownMsg(md)
}
}
// loadMarkdownFromCharm performs the actual I/O for loading markdown from the
// sever.
func loadMarkdownFromCharm(cc *charm.Client, id int, t DocType) (*markdown, error) {
var md *charm.Markdown
var err error
switch t {
case StashedDoc, ConvertedDoc:
md, err = cc.GetStashMarkdown(id)
case NewsDoc:
md, err = cc.GetNewsMarkdown(id)
default:
err = fmt.Errorf("unknown markdown type: %s", t)
}
if err != nil {
return nil, err
}
return &markdown{
markdownType: t,
Markdown: *md,
}, nil
}
func loadLocalMarkdown(md *markdown) tea.Cmd {
return func() tea.Msg {
if md.markdownType != LocalDoc {
@ -1287,7 +1317,7 @@ func deleteMarkdown(markdowns []*markdown, target *markdown) ([]*markdown, error
index = i
}
default:
return nil, fmt.Errorf("%s documents cannot be deleted", target.markdownType.String())
return nil, fmt.Errorf("%s documents cannot be deleted", target.markdownType)
}
}

View file

@ -30,6 +30,8 @@ var (
config Config
glowLogoTextColor = common.Color("#ECFD65")
debug = false // true if we're logging to a file, in which case we'll log more stuff
stashableDocTypes = NewDocTypeSet(LocalDoc, NewsDoc)
)
// Config contains TUI-specific configuration.
@ -649,26 +651,40 @@ func stashDocument(cc *charm.Client, md markdown) tea.Cmd {
}
// Is the document missing a body? If so, it likely means it needs to
// be loaded. If the document body is really empty then we'll still
// stash it.
// be loaded. But...if it turnsout the document body really is empty
// then we'll stash it anyway.
if len(md.Body) == 0 {
data, err := ioutil.ReadFile(md.localPath)
if err != nil {
if debug {
log.Println("error loading doucument body for stashing:", err)
switch md.markdownType {
case LocalDoc:
data, err := ioutil.ReadFile(md.localPath)
if err != nil {
if debug {
log.Println("error loading document body for stashing:", err)
}
return stashErrMsg{err}
}
md.Body = string(data)
case NewsDoc:
newMD, err := loadMarkdownFromCharm(cc, md.ID, md.markdownType)
if err != nil {
return stashErrMsg{err}
}
md.Body = newMD.Body
default:
if debug {
log.Printf("user is attempting to stash an unsupported markdown type: %s", md.markdownType)
}
return stashErrMsg{err}
}
md.Body = string(data)
}
// Turn local markdown into a newly stashed (converted) markdown
md.markdownType = ConvertedDoc
md.CreatedAt = time.Now()
// Set the note as the filename without the extension
p := md.localPath
md.Note = strings.Replace(path.Base(p), path.Ext(p), "", 1)
if md.markdownType == LocalDoc {
p := md.localPath
md.Note = strings.Replace(path.Base(p), path.Ext(p), "", 1)
}
newMd, err := cc.StashMarkdown(md.Note, md.Body)
if err != nil {
@ -678,9 +694,15 @@ func stashDocument(cc *charm.Client, md markdown) tea.Cmd {
return stashErrMsg{err}
}
// We really just need to know the ID so we can operate on this newly
// stashed markdown.
// The server sends the whole stashed document back, but we really just
// need to know the ID so we can operate on this newly stashed
// markdown.
md.ID = newMd.ID
// Turn the markdown into a newly stashed (converted) markdown
md.markdownType = ConvertedDoc
md.CreatedAt = time.Now()
return stashSuccessMsg(md)
}
}
@ -729,6 +751,9 @@ func indent(s string, n int) string {
}
func truncate(str string, num int) string {
if num < 1 {
return str
}
return runewidth.Truncate(str, num, "…")
}