-
Notifications
You must be signed in to change notification settings - Fork 2k
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
Add tags
support to app
#1524
Closed
Closed
Add tags
support to app
#1524
Changes from all commits
Commits
Show all changes
71 commits
Select commit
Hold shift + click to select a range
47273cc
first pass at a tag input in composer
estrattonbailey 763edc8
add TagDecorator plugin
estrattonbailey f9914d6
send along tags with api request
estrattonbailey 4399405
add tags view
estrattonbailey bfdbf43
link out tags
estrattonbailey 44694ab
improve hash handling
estrattonbailey 250e5eb
remove extra filter
estrattonbailey dab7931
made 8 default max
estrattonbailey 9668102
fix helper text
estrattonbailey 6618167
integrate into native text input
estrattonbailey bb8a016
bump api package to get new richtext
estrattonbailey 76eba7b
bump api package
estrattonbailey f404f52
clean up handling
estrattonbailey 0648f02
remove half-baked onBlur handling
estrattonbailey f5e793d
fix tag index parsing
estrattonbailey 4b47380
fix node walking
estrattonbailey a07542a
WIP autocomplete
estrattonbailey bd9c162
some tag styles
estrattonbailey 6f6a34c
clean up tags autocomplete desktop
estrattonbailey 4868d48
stick tag input to bottom
estrattonbailey 4d367d6
update graphemes
estrattonbailey 564a654
mobile autocomplete
estrattonbailey e0c614b
split tags on spaces in TagInput
estrattonbailey 804a393
move Tag, update styling
estrattonbailey 16206df
exploration of tag styles
estrattonbailey 6c25ca8
Merge remote-tracking branch 'origin/main' into eric/app-864-integrat…
estrattonbailey 3bcce36
Merge remote-tracking branch 'origin' into eric/app-864-integrate-pos…
estrattonbailey 7bef0ec
add tags autocomplete
estrattonbailey 48f15b7
install fork of suggestion plugin
estrattonbailey c3ec1e6
tag style updates on web
estrattonbailey 3637676
use consistent styling in the composer
estrattonbailey 1161050
oops
estrattonbailey d9fc277
pretty good spot with styles
estrattonbailey 5033520
generally consistent across web/ios
estrattonbailey f97f130
fix added space on commit
estrattonbailey 41c4b4a
enforce 64 characters
estrattonbailey b3bae78
enforce length in TagInput
estrattonbailey 1dc38a8
add a test
estrattonbailey b33f70f
remove unused TagDecorator
estrattonbailey 3bae374
some comments
estrattonbailey 4e26c97
consolidate tag components
estrattonbailey 7a97c3c
Merge remote-tracking branch 'origin' into eric/app-864-integrate-pos…
estrattonbailey 078ba4c
add tag highlighting back
estrattonbailey bfdf41a
cleaning
estrattonbailey 9f9f877
add desktop TagInput autocomplete
estrattonbailey d7f7cb3
mobile tags input and autocomplete
estrattonbailey 8aeb364
remove selected item code
estrattonbailey b184353
let's roll with custom bottom sheet for now
estrattonbailey 0dbf8f1
use gorhom bottom sheet
estrattonbailey 85481ef
Merge remote-tracking branch 'origin' into eric/app-864-integrate-pos…
estrattonbailey a5f37eb
move TagInput into dir, use button on desktop
estrattonbailey 7ee34b6
swap in new button
estrattonbailey 35dd618
use separate models, commit once
estrattonbailey cb81f46
Merge remote-tracking branch 'origin/main' into eric/app-864-integrat…
estrattonbailey 911a5a5
strip trailing punctuation
estrattonbailey 72bb3cc
handle focus on tag buttons
estrattonbailey 2d62a49
centralize regex
estrattonbailey d93587a
don't backfill tags, close dropdown on tab
estrattonbailey c3b500d
remove comment
estrattonbailey 4505c2b
remove unused EditableTag
estrattonbailey a5d4cfd
consolidate defs
estrattonbailey 61f88ae
inherit outline tags of parent post
estrattonbailey ee3d1ff
Use new more restrictive regex
estrattonbailey c500595
Use api package regexes
estrattonbailey ebd39c7
clean up sanitization
estrattonbailey 94e2d0b
Small tweaks
estrattonbailey cc3e992
remove unused file
estrattonbailey b78ec89
Use new api pkg APIs
estrattonbailey 8314f90
Improve autocomplete model logic
estrattonbailey 0ec579b
Tweak rendering of hashtags and expanded posts for clarity and inform…
pfrazee 38fd222
Improve overflow spacing
pfrazee File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
import {makeAutoObservable, runInAction} from 'mobx' | ||
import AwaitLock from 'await-lock' | ||
import {RootStoreModel} from '../root-store' | ||
import Fuse from 'fuse.js' | ||
import {isObj, hasProp, isStrArray} from 'lib/type-guards' | ||
|
||
/** | ||
* Used only to persist recent tags across app restarts. | ||
*/ | ||
export class RecentTagsModel { | ||
_tags: string[] = [] | ||
|
||
constructor() { | ||
makeAutoObservable(this, {}, {autoBind: true}) | ||
} | ||
|
||
get tags() { | ||
return this._tags | ||
} | ||
|
||
add(tag: string) { | ||
this._tags = Array.from(new Set([tag, ...this._tags])).slice(0, 100) // save up to 100 recent tags | ||
} | ||
|
||
remove(tag: string) { | ||
this._tags = this._tags.filter(t => t !== tag) | ||
} | ||
|
||
serialize() { | ||
return {_tags: this._tags} | ||
} | ||
|
||
hydrate(v: unknown) { | ||
if (isObj(v) && hasProp(v, '_tags') && isStrArray(v._tags)) { | ||
this._tags = Array.from(new Set(v._tags)) | ||
} | ||
} | ||
} | ||
|
||
export class TagsAutocompleteModel { | ||
lock = new AwaitLock() | ||
isActive = false | ||
query = '' | ||
searchedTags: string[] = [] | ||
profileTags: string[] = [] | ||
|
||
constructor(public rootStore: RootStoreModel) { | ||
makeAutoObservable( | ||
this, | ||
{ | ||
rootStore: false, | ||
}, | ||
{autoBind: true}, | ||
) | ||
} | ||
|
||
setActive(isActive: boolean) { | ||
this.isActive = isActive | ||
} | ||
|
||
commitRecentTag(tag: string) { | ||
this.rootStore.recentTags.add(tag) | ||
} | ||
|
||
clear() { | ||
this.query = '' | ||
this.searchedTags = [] | ||
} | ||
|
||
get suggestions() { | ||
if (!this.isActive) { | ||
return [] | ||
} | ||
|
||
// no query, return default suggestions | ||
if (!this.query) { | ||
return Array.from( | ||
// de-duplicates via Set | ||
new Set([ | ||
// sample 6 recent tags | ||
...this.rootStore.recentTags.tags.slice(0, 6), | ||
// sample 3 of your profile tags | ||
...this.profileTags.slice(0, 3), | ||
]), | ||
) | ||
} | ||
|
||
// we're going to search this list | ||
const items = Array.from( | ||
// de-duplicates via Set | ||
new Set([ | ||
// all recent tags | ||
...this.rootStore.recentTags.tags, | ||
// all profile tags | ||
...this.profileTags, | ||
// and all searched tags | ||
...this.searchedTags, | ||
]), | ||
) | ||
|
||
// Fuse allows weighting values too, if we ever need it | ||
const fuse = new Fuse(items) | ||
// search amongst mixed set of tags | ||
const results = fuse.search(this.query).map(r => r.item) | ||
return results.slice(0, 9) | ||
} | ||
|
||
async search(query: string) { | ||
this.query = query.trim() | ||
|
||
await this.lock.acquireAsync() | ||
|
||
try { | ||
await this._search() | ||
} finally { | ||
this.lock.release() | ||
} | ||
} | ||
|
||
// TODO hook up to search type-ahead | ||
async _search() { | ||
runInAction(() => { | ||
this.searchedTags = [] | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
import React from 'react' | ||
import {StyleSheet, Pressable, StyleProp, TextStyle} from 'react-native' | ||
import { | ||
FontAwesomeIcon, | ||
FontAwesomeIconStyle, | ||
} from '@fortawesome/react-native-fontawesome' | ||
|
||
import {isWeb} from 'platform/detection' | ||
import {usePalette} from 'lib/hooks/usePalette' | ||
import {useTheme} from 'lib/ThemeContext' | ||
import {Text, CustomTextProps} from 'view/com/util/text/Text' | ||
import {TextLink} from 'view/com/util/Link' | ||
|
||
export function Tag({ | ||
value, | ||
textSize, | ||
smallSigil, | ||
style, | ||
}: { | ||
value: string | ||
textSize?: CustomTextProps['type'] | ||
smallSigil?: boolean | ||
style?: StyleProp<TextStyle> | ||
}) { | ||
const theme = useTheme() | ||
const type = textSize || 'xs-medium' | ||
const typeFontSize = theme.typography[type].fontSize || 16 | ||
const hashtagFontSize = typeFontSize * (smallSigil ? 0.8 : 1) | ||
|
||
return ( | ||
<TextLink | ||
type={type} | ||
text={`#${value}`} | ||
accessible | ||
href={`/search?q=${value}`} | ||
style={style}> | ||
<Text | ||
style={[ | ||
style, | ||
{ | ||
fontSize: hashtagFontSize, | ||
fontWeight: '500', | ||
}, | ||
]}> | ||
# | ||
</Text> | ||
{value} | ||
</TextLink> | ||
) | ||
} | ||
|
||
export function TagButton({ | ||
value, | ||
icon = 'x', | ||
onClick, | ||
removeTag, | ||
}: { | ||
value: string | ||
icon?: React.ComponentProps<typeof FontAwesomeIcon>['icon'] | ||
onClick?: (tag: string) => void | ||
removeTag?: (tag: string) => void | ||
}) { | ||
const pal = usePalette('default') | ||
const [hovered, setHovered] = React.useState(false) | ||
const [focused, setFocused] = React.useState(false) | ||
|
||
const hoverIn = React.useCallback(() => { | ||
setHovered(true) | ||
}, [setHovered]) | ||
|
||
const hoverOut = React.useCallback(() => { | ||
setHovered(false) | ||
}, [setHovered]) | ||
|
||
React.useEffect(() => { | ||
if (!isWeb) return | ||
|
||
function listener(e: KeyboardEvent) { | ||
if (e.key === 'Backspace') { | ||
if (focused) { | ||
removeTag?.(value) | ||
} | ||
} | ||
} | ||
|
||
document.addEventListener('keydown', listener) | ||
|
||
return () => { | ||
document.removeEventListener('keydown', listener) | ||
} | ||
}, [value, focused, removeTag]) | ||
|
||
return ( | ||
<Pressable | ||
accessibilityRole="button" | ||
onPress={() => onClick?.(value)} | ||
onPointerEnter={hoverIn} | ||
onPointerLeave={hoverOut} | ||
onFocus={() => setFocused(true)} | ||
onBlur={() => setFocused(false)} | ||
style={state => [ | ||
pal.viewLight, | ||
styles.tagButton, | ||
{ | ||
outline: 0, | ||
opacity: state.pressed || state.focused ? 0.6 : 1, | ||
paddingRight: 10, | ||
}, | ||
]}> | ||
<Text type="md-medium" style={[pal.textLight]}> | ||
#{value} | ||
</Text> | ||
<FontAwesomeIcon | ||
icon={icon} | ||
style={ | ||
{ | ||
opacity: hovered ? 1 : 0.5, | ||
color: pal.textLight.color, | ||
marginTop: 1, | ||
} as FontAwesomeIconStyle | ||
} | ||
size={10} | ||
/> | ||
</Pressable> | ||
) | ||
} | ||
|
||
const styles = StyleSheet.create({ | ||
editableTag: { | ||
flexDirection: 'row', | ||
alignItems: 'center', | ||
gap: 6, | ||
paddingTop: 4, | ||
paddingBottom: 4, | ||
paddingHorizontal: 8, | ||
borderRadius: 4, | ||
overflow: 'hidden', | ||
}, | ||
tagButton: { | ||
flexDirection: 'row', | ||
alignItems: 'center', | ||
gap: 8, | ||
flexShrink: 1, | ||
paddingVertical: 6, | ||
paddingTop: 5, | ||
paddingHorizontal: 12, | ||
borderRadius: 20, | ||
}, | ||
}) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Not currently isolated per-account
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.
Or persisted across devices