Skip to content

clients: support async page fetching #614

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
34 changes: 26 additions & 8 deletions brclient/appstate.go
Original file line number Diff line number Diff line change
Expand Up @@ -1302,9 +1302,8 @@ func (as *appState) findPagesChatWindow(sessID clientintf.PagesSessionID) *chatW
return nil
}

func (as *appState) findOrNewPagesChatWindow(sessID clientintf.PagesSessionID) *chatWindow {
func (as *appState) findOrNewPagesChatWindow(sessID clientintf.PagesSessionID) (cw *chatWindow, isNew bool) {
as.chatWindowsMtx.Lock()
var cw *chatWindow
for i, acw := range as.chatWindows {
if acw.isPage && acw.pageSess == sessID {
if i != as.activeCW {
Expand All @@ -1324,10 +1323,11 @@ func (as *appState) findOrNewPagesChatWindow(sessID clientintf.PagesSessionID) *
}
as.chatWindows = append(as.chatWindows, cw)
as.updatedCW[len(as.chatWindows)-1] = false
isNew = true
}
as.chatWindowsMtx.Unlock()
as.footerInvalidate()
return cw
return
}

// markWindowUpdated marks the window as updated.
Expand Down Expand Up @@ -2507,7 +2507,7 @@ func (as *appState) downloadEmbed(source clientintf.UserID, embedded mdembeds.Em

// fetchPage requests the given page from the user.
func (as *appState) fetchPage(uid clientintf.UserID, pagePath string, session,
parent clientintf.PagesSessionID, form *formEl) error {
parent clientintf.PagesSessionID, form *formEl, asyncTargetId string) error {
if len(pagePath) < 1 {
return fmt.Errorf("page path is empty")
}
Expand Down Expand Up @@ -2558,14 +2558,15 @@ func (as *appState) fetchPage(uid clientintf.UserID, pagePath string, session,
return err
}

tag, err := as.c.FetchResource(uid, path, nil, session, parent, data)
tag, err := as.c.FetchResource(uid, path, nil, session, parent, data,
asyncTargetId)
if err != nil {
return err
}

// Mark session making a new request (if it already exists).
cw := as.findPagesChatWindow(session)
if cw != nil {
if cw != nil && asyncTargetId == "" {
cw.Lock()
cw.pageRequested = &path
cw.Unlock()
Expand All @@ -2574,6 +2575,9 @@ func (as *appState) fetchPage(uid clientintf.UserID, pagePath string, session,
// Initialize the page spinner.
as.sendMsg(msgActiveCWRequestedPage{[]tea.Cmd{cw.pageSpinner.Tick}})
}
} else if cw != nil && asyncTargetId != "" {
cw.replaceAsyncTargetWithLoading(asyncTargetId)
as.repaintIfActive(cw)
}

as.diagMsg("Attempting to fetch %s from %s (session %s, tag %s)",
Expand Down Expand Up @@ -3246,8 +3250,22 @@ func newAppState(sendMsg func(tea.Msg), lndLogLines *sloglinesbuffer.Buffer,
return
}

cw := as.findOrNewPagesChatWindow(fr.SessionID)
cw.replacePage(nick, fr)
cw, isNew := as.findOrNewPagesChatWindow(fr.SessionID)

// When this is the response to an async request and this is the
// first time this page is opened, load the entire history of
// the page and its async requests.
var history []*clientdb.FetchedResource
if isNew && fr.AsyncTargetID != "" {
history, err = as.c.LoadFetchedResource(uid, fr.SessionID, fr.ParentPage)
if err != nil {
as.diagMsg("Error loading history for page %s/%s: %v",
fr.SessionID, fr.ParentPage, err)
return
}
}

cw.replacePage(nick, fr, history)
sendMsg(msgPageFetched{
uid: uid,
nick: nick,
Expand Down
139 changes: 135 additions & 4 deletions brclient/chatwindow.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,20 @@ type formEl struct {
fields []*formField
}

// asyncTarget returns the id of the target when the form has an asynctarget
// field.
func (f *formEl) asyncTarget() string {
for _, ff := range f.fields {
if ff.typ == "asynctarget" && ff.value != nil {
if target, ok := ff.value.(string); ok {
return target
}
}
}

return ""
}

func (f *formEl) action() string {
for _, ff := range f.fields {
if ff.typ == "action" && ff.value != nil {
Expand Down Expand Up @@ -407,6 +421,11 @@ func parseMsgLine(line string, mention string) *chatMsgElLine {
return res
}

var (
sectionStartRegexp = regexp.MustCompile(`--section id=([\w]+) --`)
sectionEndRegexp = regexp.MustCompile(`--/section--`)
)

func parseMsgIntoElements(msg string, mention string) []*chatMsgElLine {
// First, break into lines.
lines := strings.Split(msg, "\n")
Expand All @@ -418,6 +437,10 @@ func parseMsgIntoElements(msg string, mention string) []*chatMsgElLine {
form = &formEl{}
case line == "--/form--":
form = nil
case sectionStartRegexp.MatchString(line):
// Skip section start line
case sectionEndRegexp.MatchString(line):
// Skip section end line
case form != nil:
ff := parseFormField(line)
if ff != nil {
Expand All @@ -429,6 +452,7 @@ func parseMsgIntoElements(msg string, mention string) []*chatMsgElLine {
el.PushBack(msgEl)
res = append(res, el)
}

default:
res = append(res, parseMsgLine(line, mention))
}
Expand Down Expand Up @@ -564,7 +588,54 @@ func (cw *chatWindow) newRecvdMsg(from, msg string, fromUID *zkidentity.ShortID,
return m
}

func (cw *chatWindow) replacePage(nick string, fr clientdb.FetchedResource) {
func (cw *chatWindow) replaceAsyncTargetWithLoading(asyncTargetID string) {
cw.Lock()
defer cw.Unlock()

if len(cw.msgs) == 0 {
return
}
if cw.page == nil {
return
}

data := cw.page.Response.Data

reStartPattern := `--section id=` + asyncTargetID + ` --\n`
reStart, err := regexp.Compile(reStartPattern)
if err != nil {
// Skip invalid ids.
return
}

startPos := reStart.FindIndex(data)
if startPos == nil {
// Did not find the target location.
return
}

endPos := sectionEndRegexp.FindIndex(data[startPos[1]:])
if endPos == nil {
// Unterminated section.
return
}
endPos[0] += startPos[1] // Convert to absolute index

// Copy the rest of the string to an aux buffer.
aux := append([]byte(nil), data[endPos[0]:]...)

// Create the new buffer, replacing the contents inside
// the section with this response.
data = data[0:startPos[1]]
data = append(data, []byte("(⏳ Loading response)\n")...)
data = append(data, aux...)

msg := cw.msgs[0]
msg.elements = parseMsgIntoElements(string(data), "")
cw.page.Response.Data = data
}

func (cw *chatWindow) replacePage(nick string, fr clientdb.FetchedResource, history []*clientdb.FetchedResource) {
cw.Lock()
var msg *chatMsg
if len(cw.msgs) == 0 {
Expand All @@ -573,10 +644,70 @@ func (cw *chatWindow) replacePage(nick string, fr clientdb.FetchedResource) {
} else {
msg = cw.msgs[0]
}
msg.elements = parseMsgIntoElements(string(fr.Response.Data), "")

// Replace async targets.
var data, aux []byte
if len(history) > 0 || fr.AsyncTargetID != "" {
// If there is history, this is loading from disk, so use only
// whats in the slice. Otherwise, replace the response data.
var toProcess []*clientdb.FetchedResource
if len(history) > 0 {
data = history[0].Response.Data
toProcess = history[1:]
} else {
data = cw.page.Response.Data
toProcess = []*clientdb.FetchedResource{&fr}
}

// Process the async targets.
for _, asyncRes := range toProcess {
reStartPattern := `--section id=` + asyncRes.AsyncTargetID + ` --\n`
reStart, err := regexp.Compile(reStartPattern)
if err != nil {
// Skip invalid ids.
continue
}

startPos := reStart.FindIndex(data)
if startPos == nil {
// Did not find the target location.
continue
}

endPos := sectionEndRegexp.FindIndex(data[startPos[1]:])
if endPos == nil {
// Unterminated section.
continue
}
endPos[0] += startPos[1] // Convert to absolute index

// Copy the rest of the string to an aux buffer.
aux = append(aux, data[endPos[0]:]...)

// Create the new buffer, replacing the contents inside
// the section with this response.
data = data[0:startPos[1]]
data = append(data, asyncRes.Response.Data...)
data = append(data, aux...)
aux = aux[:0]
}
} else {
data = fr.Response.Data
}

msg.elements = parseMsgIntoElements(string(data), "")
msg.fromUID = &fr.UID
cw.page = &fr
cw.selElIndex = 0
if len(history) > 0 {
cw.page = history[0]
} else if fr.AsyncTargetID == "" {
cw.page = &fr
}
cw.page.Response.Data = data
if history != nil || fr.AsyncTargetID == "" {
// Only reset the selected element index when replacing the
// entire page.
cw.selElIndex = 0
}
cw.pageRequested = nil
cw.alias = fmt.Sprintf("%v/%v", nick, strings.Join(fr.Request.Path, "/"))
cw.Unlock()
Expand Down
4 changes: 2 additions & 2 deletions brclient/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -3147,7 +3147,7 @@ var pagesCommands = []tuicmd{
if len(args) > 1 {
pagePath = strings.TrimSpace(args[1])
}
return as.fetchPage(uid, pagePath, nextSess, 0, nil)
return as.fetchPage(uid, pagePath, nextSess, 0, nil, "")
},
completer: func(args []string, arg string, as *appState) []string {
if len(args) == 0 {
Expand All @@ -3169,7 +3169,7 @@ var pagesCommands = []tuicmd{
// Always use the same session ID for convenience when
// fetching a local page.
nextSess := clientintf.PagesSessionID(0)
return as.fetchPage(as.c.PublicID(), pagePath, nextSess, 0, nil)
return as.fetchPage(as.c.PublicID(), pagePath, nextSess, 0, nil, "")
},
},
}
Expand Down
10 changes: 6 additions & 4 deletions brclient/mainwindow.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ func (mws mainWindowState) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// Navigate to other page.
uid := cw.page.UID
err := mws.as.fetchPage(uid, *cw.selEl.link,
cw.page.SessionID, cw.page.PageID, nil)
cw.page.SessionID, cw.page.PageID, nil, "")
if err != nil {
mws.as.diagMsg("Unable to fetch page: %v", err)
}
Expand All @@ -316,7 +316,8 @@ func (mws mainWindowState) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
action := cw.selEl.form.action()

err := mws.as.fetchPage(uid, action,
cw.page.SessionID, cw.page.PageID, cw.selEl.form)
cw.page.SessionID, cw.page.PageID,
cw.selEl.form, cw.selEl.form.asyncTarget())
if err != nil {
mws.as.diagMsg("Unable to fetch page: %v", err)
}
Expand Down Expand Up @@ -509,7 +510,7 @@ func (mws mainWindowState) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// Navigate to other page.
uid := cw.page.UID
err := mws.as.fetchPage(uid, *cw.selEl.link,
cw.page.SessionID, cw.page.PageID, nil)
cw.page.SessionID, cw.page.PageID, nil, "")
if err != nil {
mws.as.diagMsg("Unable to fetch page: %v", err)
}
Expand All @@ -525,7 +526,8 @@ func (mws mainWindowState) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// }

err := mws.as.fetchPage(uid, action,
cw.page.SessionID, cw.page.PageID, cw.selEl.form)
cw.page.SessionID, cw.page.PageID, cw.selEl.form,
cw.selEl.form.asyncTarget())
if err != nil {
mws.as.diagMsg("Unable to fetch page: %v", err)
}
Expand Down
2 changes: 1 addition & 1 deletion bruig/flutterui/bruig/lib/components/md_elements.dart
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ class MarkdownArea extends StatelessWidget {
var parentPageID = pageSource?.pageID ?? 0;
try {
await resources.fetchPage(
uid, parsed.pathSegments, sessionID, parentPageID, null);
uid, parsed.pathSegments, sessionID, parentPageID, null, "");
} catch (exception) {
snackbar.error("Unable to fetch page: $exception");
}
Expand Down
10 changes: 8 additions & 2 deletions bruig/flutterui/bruig/lib/components/pages/forms.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,15 @@ class _FormSubmitButton extends StatelessWidget {
var snackbar = SnackBarModel.of(context);
Map<String, dynamic> formData = {};
String action = "";
String asyncTargetID = "";
for (var field in form.fields) {
if (field.type == "action") {
action = field.value ?? "";
}
if (field.type == "asynctarget") {
asyncTargetID = field.value ?? "";
continue;
}
if (field.name == "" || field.value == null) {
continue;
}
Expand All @@ -44,8 +49,8 @@ class _FormSubmitButton extends StatelessWidget {
var parentPageID = pageSource?.pageID ?? 0;

try {
await resources.fetchPage(
uid, parsed.pathSegments, sessionID, parentPageID, formData);
await resources.fetchPage(uid, parsed.pathSegments, sessionID,
parentPageID, formData, asyncTargetID);
} catch (exception) {
snackbar.error("Unable to fetch page: $exception");
}
Expand Down Expand Up @@ -163,6 +168,7 @@ class CustomFormState extends State<CustomForm> {
case "submit":
submit = field;
break;
case "asynctarget":
case "hidden":
case "action":
break;
Expand Down
2 changes: 1 addition & 1 deletion bruig/flutterui/bruig/lib/models/menus.dart
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ List<ChatMenuItem> buildUserChatMenu(ChatModel chat) {
var path = ["index.md"];
try {
var resources = Provider.of<ResourcesModel>(context, listen: false);
var sess = await resources.fetchPage(chat.id, path, 0, 0, null);
var sess = await resources.fetchPage(chat.id, path, 0, 0, null, "");
var event = RequestedResourceEvent(chat.id, sess);
chat.append(ChatEventModel(event, null), false);
} catch (exception) {
Expand Down
Loading
Loading