-
Notifications
You must be signed in to change notification settings - Fork 3
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
feat: Add popups to start new conversations #76
Conversation
After a long discussion on Slack yesterday, we have come up with a better UI idea. To avoid shifts, and to allow suggestions reusability, here is the layout we want to create: I made this quickly in SwiftUI, so it's not perfect, but you get the idea. One major problem with using vanilla SwiftUI is that we loose out-of-the-box accessibility, keyboard navigation and support for text field tokens, which we'd like to use. Today I'm looking at using native AppKit components, and making this view more reusable so we can use it in other places (everywhere we need to search for a chat address). I will use this PR to document my process, and paste all the interesting links I found. |
Useful links:
|
I'm having a hard time trying to wire the |
Here is what I could get: ✅ For now we can:
✨ Also:
📝 TODO:
It was a lot of work, for not much code, but I'm sure we can make this very useful and portable. |
Oh! I'm thinking the entire list could be SwiftUI 💡 This would be far easier and more flexible than having an AppKit We could even show something other than a list if we wanted (e.g. add a header/footer) 💡 |
I thought about this again, and I'm not sure it'll be easy to keep the keyboard navigation if the list comes from SwiftUI. Passing a binding to the selected row could do it, I'll try. |
OK, I managed to get a lot further 👍🏼 With a custom Here, we loose the native benefits of With a
|
I cleaned up the code a lot. I still have some cleanup to do, then I'll make it so tapping on a row selects it (+ hover style?). After that I'll integrate TCA. I should have done most of this by tonight. |
I'll try to have a look at tokens tonight, and do the TCA wrapping tomorrow |
After a short discussion on Slack yesterday, we decided I would look at tokens soon, but do the TCA wrapping first. Here is what I came up with: Screen.Recording.2022-06-30.at.11.28.03.movThere are still minor things to fix, but it's going in the right direction! |
Support for sections + loading from TCA environment: Screen.Recording.2022-06-30.at.15.35.57.mov |
I didn't try, but you should be able to navigate across sections (there is no reason this would not work). |
Phew, ok. Great that it works with the combination of TextAttachments and SwiftUI views and windows and what have you! There's a lot going on which took a great deal of experimentation and research. Well done! Instead of moving on with this PR, I think we should first try to find the right abstractions for these components. In my opinion it would make sense to create separate new PRs where you pull over the respective code, possibly creating multiple stacked PRs with each component. Here's my suggestion for what I think should be 4 components:
The benefit of splitting the functionality into smaller bits is that we can test each very easily and compose them into bigger components, like the actual ContactSuggestionField. They will also be easier to understand since they only deal with one thing and we can also configure them for our different use-cases. E.g. the contact field in the multi-user chat which probably wraps and grows up until to a certain number of lines. Or the sidebar in the same view which presents its search results inline instead of in a window (see below). Here are the features of each component: 1. TCA-driven TextView + Reducer
Thruth be told, we'll certainly need multiple versions of this. The field in the sidebar will probably wrap a 2. AutoSuggest reducer and environment
2.1 AutoSuggest listWe could either provide a ready-made list which displays AutoSuggestSections or built it from scratch when needed. The ready-made list would…
It would come with a tiny state wrapper: @dynamicMemberLookup struct AutoSuggestListState<T: Hashable & Identifiable>: Equatable {
var selection: T.ID?
var state: AutoSuggestState<T>
subscript<U>(dynamicMember keyPath: WritableKeyPath<AutoSuggestState<T>, U>) -> U {
get { self.state[keyPath: keyPath] }
set { self.state[keyPath: keyPath] = newValue }
}
} 3. SwiftUI window
4. ContactSuggestionField (or actually just a reducer)
The usage could look like this: struct AddContactScreen: View {
let store: Store<ContactAutoSuggestState, ContactAutoSuggestAction>
var body: some View {
WithViewStore(self.store) { viewStore in
TCATextView(self.store.scope(state: \.textView, action: ContactAutoSuggestAction.textView))
.drawsFocusRing(true)
.cornerRadius(8)
.attachedWindow(isPresented: viewStore.binding(\.$isSuggesting)) {
AutoSuggestList(store: self.store.scope(
state: \.suggestionList,
action: ContactAutoSuggestAction.autoSuggest
)) { contact in
HStack {
Text(verbatim: contact.name).bold()
Text("—")
Text(verbatim: contact.jid)
}
}
}
}
}
} and just a little more context… struct TCATextViewState: Equatable {
var text: NSAttributedString
var selectedRange: NSRange?
}
enum TCATextViewAction: Equatable {
case textDidChange(NSAttributedString)
case selectionDidChange(NSRange)
case keyboardEventReceived(KeyEvent)
}
public enum KeyEvent: Equatable {
case up
case down
case newline
case escape
}
enum Loadable<T: Equatable>: Equatable {
case notRequested
case loading(previous: T)
case loaded(T)
case error(EquatableError, previous: T)
}
struct AutoSuggestState<T: Hashable & Identifiable>: Equatable {
var content: Loadable<[AutoSuggestSection<T>]>
// …
}
@dynamicMemberLookup struct AutoSuggestListState<T: Hashable & Identifiable>: Equatable {
var selection: T.ID?
var state: AutoSuggestState<T>
subscript<U>(dynamicMember keyPath: WritableKeyPath<AutoSuggestState<T>, U>) -> U {
get { self.state[keyPath: keyPath] }
set { self.state[keyPath: keyPath] = newValue }
}
}
enum AutoSuggestAction<T: Hashable & Identifiable>: Equatable {
case searchQueryChanged(String?)
case itemSelected(T)
case autoSuggestResponse(Result<[AutoSuggestSection<T>], EquatableError>)
case retryButtonTapped
// …
}
struct ContactAutoSuggestState: Equatable {
var textView: TCATextViewState
var suggestionList: AutoSuggestListState<Contact>
@BindableState var isSuggesting = false
// …
}
enum ContactAutoSuggestAction: BindableAction, Equatable {
case textView(TCATextViewAction)
case autoSuggest(AutoSuggestAction<Contact>)
case binding(BindingAction<ContactAutoSuggestState>)
// …
} Regarding the behavior of the ContactSuggestionField, I think we're still a little fuzzy there. You can easily end up with orphaned text which wasn't converted into a token. The popover will then also not appear anymore. I think we should clarify with @valeriansaliou what the expected behavior is. |
@nesium providing more details on the expected behavior of the contact picker in this view:
|
(I will answer here, even though the code has moved somewhere else.) Thank you for this very long proposition/study 🤩 I agree with most of what you said, except the few things I noted down there 👇 I think It will require a lot of time to implement all of this, and I don't think it's the most required feature right now. I split this PR in two, so we can have a basic text field with only strings, which can be submitted only if the JID matches some known JID. Once we can start chats and all, I'll come back to #78. Just typing what crosses my mind:
|
The problem with reference types in a TCA state is that you cannot detect changes when you observe the store via a |
You're right, that's (one of the many reasons) why I don't like my current implementation 😕 (Did you know |
@nesium Do we need such text field in multiline? (I'm asking because we intercept the return key event, which prevents the user from typing a newline.) |
336e2ec
to
15cdc54
Compare
@nesium I had to rebase, and by rebasing it updated |
I really didn't like doing height computations with "magic numbers", and I just found a way to avoid them 🥳 Now it will always fit the content, even with multiple row sizes or a completely different content 🙂
3b99807
to
a1b5d61
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Two things:
- Please add the
SearchSuggestionsFeaturePreview
to the Makefile so that it gets built with the others. - Can we get rid of the fake validation/delays to not waste time unneccessarily during testing?
The code from this PR was originally in another PR, |
e043253
to
00e383c
Compare
The "Add member" popup:
What we have now:
The "Create or join group" popup
What we have now:
Nothing