mirror of
				https://github.com/tooot-app/app
				synced 2025-06-05 22:19:13 +02:00 
			
		
		
		
	Basic auto suggestion is working
This commit is contained in:
		
							
								
								
									
										4150
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4150
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -9,7 +9,7 @@ const client = async ({ | |||||||
|   query, |   query, | ||||||
|   body |   body | ||||||
| }: { | }: { | ||||||
|   version: 'v1' | 'v2' |   version?: 'v1' | 'v2' | ||||||
|   method: 'get' | 'post' | 'delete' |   method: 'get' | 'post' | 'delete' | ||||||
|   instance: 'local' | 'remote' |   instance: 'local' | 'remote' | ||||||
|   endpoint: string |   endpoint: string | ||||||
| @@ -34,8 +34,8 @@ const client = async ({ | |||||||
|       }, |       }, | ||||||
|       ...(body && { json: body }) |       ...(body && { json: body }) | ||||||
|     }) |     }) | ||||||
|   } catch { |   } catch (error) { | ||||||
|     return Promise.reject('ky error') |     return Promise.reject('ky error: ' + error) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (response.ok) { |   if (response.ok) { | ||||||
|   | |||||||
| @@ -45,18 +45,20 @@ const Attachment: React.FC<Props> = ({ | |||||||
|         (width / media_attachments[0].meta.original.width) * |         (width / media_attachments[0].meta.original.width) * | ||||||
|         media_attachments[0].meta.original.height |         media_attachments[0].meta.original.height | ||||||
|       break |       break | ||||||
|     case 'video': |     // Support multiple video | ||||||
|       attachment = ( |     // Supoort when video meta is empty | ||||||
|         <AttachmentVideo |     // case 'video': | ||||||
|           media_attachments={media_attachments} |     //   attachment = ( | ||||||
|           sensitive={sensitive} |     //     <AttachmentVideo | ||||||
|           width={width} |     //       media_attachments={media_attachments} | ||||||
|         /> |     //       sensitive={sensitive} | ||||||
|       ) |     //       width={width} | ||||||
|       attachmentHeight = |     //     /> | ||||||
|         (width / media_attachments[0].meta.original.width) * |     //   ) | ||||||
|         media_attachments[0].meta.original.height |     //   attachmentHeight = | ||||||
|       break |     //     (width / media_attachments[0].meta.original.width) * | ||||||
|  |     //     media_attachments[0].meta.original.height | ||||||
|  |     //   break | ||||||
|     // case 'audio': |     // case 'audio': | ||||||
|     //   attachment = ( |     //   attachment = ( | ||||||
|     //     <AttachmentAudio |     //     <AttachmentAudio | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import { Match, MatchConfig } from "./match"; | import { Match, MatchConfig } from './match'; | ||||||
| import { MentionServices } from "../autolinker"; | import { MentionServices } from '../autolinker'; | ||||||
| /** | /** | ||||||
|  * @class Autolinker.match.Mention |  * @class Autolinker.match.Mention | ||||||
|  * @extends Autolinker.match.Match |  * @extends Autolinker.match.Match | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import * as tslib_1 from "tslib"; | import * as tslib_1 from "tslib"; | ||||||
| import { Match } from "./match"; | import { Match } from './match'; | ||||||
| /** | /** | ||||||
|  * @class Autolinker.match.Mention |  * @class Autolinker.match.Mention | ||||||
|  * @extends Autolinker.match.Match |  * @extends Autolinker.match.Match | ||||||
| @@ -67,13 +67,16 @@ var MentionMatch = /** @class */ (function (_super) { | |||||||
|      */ |      */ | ||||||
|     MentionMatch.prototype.getAnchorHref = function () { |     MentionMatch.prototype.getAnchorHref = function () { | ||||||
|         switch (this.serviceName) { |         switch (this.serviceName) { | ||||||
|  |             case 'mastodon': | ||||||
|  |                 return 'https://example.com/' + this.mention; | ||||||
|             case 'twitter': |             case 'twitter': | ||||||
|                 return 'https://twitter.com/' + this.mention; |                 return 'https://twitter.com/' + this.mention; | ||||||
|             case 'instagram': |             case 'instagram': | ||||||
|                 return 'https://instagram.com/' + this.mention; |                 return 'https://instagram.com/' + this.mention; | ||||||
|             case 'soundcloud': |             case 'soundcloud': | ||||||
|                 return 'https://soundcloud.com/' + this.mention; |                 return 'https://soundcloud.com/' + this.mention; | ||||||
|             default: // Shouldn't happen because Autolinker's constructor should block any invalid values, but just in case. |             default: | ||||||
|  |                 // Shouldn't happen because Autolinker's constructor should block any invalid values, but just in case. | ||||||
|                 throw new Error('Unknown service name to point mention to: ' + this.serviceName); |                 throw new Error('Unknown service name to point mention to: ' + this.serviceName); | ||||||
|         } |         } | ||||||
|     }; |     }; | ||||||
|   | |||||||
| @@ -1 +1 @@ | |||||||
| {"version":3,"sources":["../src/match/mention-match.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,KAAK,EAAe,MAAM,SAAS,CAAC;AAG7C;;;;;;;GAOG;AACH;IAAkC,wCAAK;IAkBtC;;;;OAIG;IACH,sBAAa,GAAuB;QAApC,YACC,kBAAO,GAAG,CAAE,SAIZ;QA1BD;;;;;WAKG;QACc,iBAAW,GAAoB,SAAS,CAAC,CAAE,gGAAgG;QAE5J;;;;WAIG;QACc,aAAO,GAAW,EAAE,CAAC,CAAE,gGAAgG;QAWvI,KAAI,CAAC,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC;QAC3B,KAAI,CAAC,WAAW,GAAG,GAAG,CAAC,WAAW,CAAC;;IACpC,CAAC;IAGD;;;;;OAKG;IACH,8BAAO,GAAP;QACC,OAAO,SAAS,CAAC;IAClB,CAAC;IAGD;;;;OAIG;IACH,iCAAU,GAAV;QACC,OAAO,IAAI,CAAC,OAAO,CAAC;IACrB,CAAC;IAGD;;;;;OAKG;IACH,qCAAc,GAAd;QACC,OAAO,IAAI,CAAC,WAAW,CAAC;IACzB,CAAC;IAGD;;;;OAIG;IACH,oCAAa,GAAb;QACC,QAAQ,IAAI,CAAC,WAAW,EAAG;YAC1B,KAAK,SAAS;gBACb,OAAO,sBAAsB,GAAG,IAAI,CAAC,OAAO,CAAC;YAC9C,KAAK,WAAW;gBACf,OAAO,wBAAwB,GAAG,IAAI,CAAC,OAAO,CAAC;YAChD,KAAK,YAAY;gBAChB,OAAO,yBAAyB,GAAG,IAAI,CAAC,OAAO,CAAC;YAEjD,SAAW,uGAAuG;gBACjH,MAAM,IAAI,KAAK,CAAE,4CAA4C,GAAG,IAAI,CAAC,WAAW,CAAE,CAAC;SACpF;IACF,CAAC;IAGD;;;;OAIG;IACH,oCAAa,GAAb;QACC,OAAO,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC;IAC3B,CAAC;IAGD;;;;;;OAMG;IACH,0CAAmB,GAAnB;QACC,IAAI,gBAAgB,GAAG,iBAAM,mBAAmB,WAAE,EAC9C,WAAW,GAAG,IAAI,CAAC,cAAc,EAAE,CAAC;QAExC,IAAI,WAAW,EAAG;YACjB,gBAAgB,CAAC,IAAI,CAAE,WAAW,CAAE,CAAC;SACrC;QACD,OAAO,gBAAgB,CAAC;IACzB,CAAC;IAEF,mBAAC;AAAD,CA9GA,AA8GC,CA9GiC,KAAK,GA8GtC","file":"mention-match.js","sourcesContent":["import { Match, MatchConfig } from \"./match\";\nimport { MentionServices } from \"../autolinker\";\n\n/**\n * @class Autolinker.match.Mention\n * @extends Autolinker.match.Match\n *\n * Represents a Mention match found in an input string which should be Autolinked.\n *\n * See this class's superclass ({@link Autolinker.match.Match}) for more details.\n */\nexport class MentionMatch extends Match {\n\n\t/**\n\t * @cfg {String} serviceName\n\t *\n\t * The service to point mention matches to. See {@link Autolinker#mention}\n\t * for available values.\n\t */\n\tprivate readonly serviceName: MentionServices = 'twitter';  // default value just to get the above doc comment in the ES5 output and documentation generator\n\n\t/**\n\t * @cfg {String} mention (required)\n\t *\n\t * The Mention that was matched, without the '@' character.\n\t */\n\tprivate readonly mention: string = '';  // default value just to get the above doc comment in the ES5 output and documentation generator\n\n\n\t/**\n\t * @method constructor\n\t * @param {Object} cfg The configuration properties for the Match\n\t *   instance, specified in an Object (map).\n\t */\n\tconstructor( cfg: MentionMatchConfig ) {\n\t\tsuper( cfg );\n\n\t\tthis.mention = cfg.mention;\n\t\tthis.serviceName = cfg.serviceName;\n\t}\n\n\n\t/**\n\t * Returns a string name for the type of match that this class represents.\n\t * For the case of MentionMatch, returns 'mention'.\n\t *\n\t * @return {String}\n\t */\n\tgetType() {\n\t\treturn 'mention';\n\t}\n\n\n\t/**\n\t * Returns the mention, without the '@' character.\n\t *\n\t * @return {String}\n\t */\n\tgetMention() {\n\t\treturn this.mention;\n\t}\n\n\n\t/**\n\t * Returns the configured {@link #serviceName} to point the mention to.\n\t * Ex: 'instagram', 'twitter', 'soundcloud'.\n\t *\n\t * @return {String}\n\t */\n\tgetServiceName() {\n\t\treturn this.serviceName;\n\t}\n\n\n\t/**\n\t * Returns the anchor href that should be generated for the match.\n\t *\n\t * @return {String}\n\t */\n\tgetAnchorHref() {\n\t\tswitch( this.serviceName ) {\n\t\t\tcase 'twitter' :\n\t\t\t\treturn 'https://twitter.com/' + this.mention;\n\t\t\tcase 'instagram' :\n\t\t\t\treturn 'https://instagram.com/' + this.mention;\n\t\t\tcase 'soundcloud' :\n\t\t\t\treturn 'https://soundcloud.com/' + this.mention;\n\n\t\t\tdefault :  // Shouldn't happen because Autolinker's constructor should block any invalid values, but just in case.\n\t\t\t\tthrow new Error( 'Unknown service name to point mention to: ' + this.serviceName );\n\t\t}\n\t}\n\n\n\t/**\n\t * Returns the anchor text that should be generated for the match.\n\t *\n\t * @return {String}\n\t */\n\tgetAnchorText() {\n\t\treturn '@' + this.mention;\n\t}\n\n\n\t/**\n\t * Returns the CSS class suffixes that should be used on a tag built with\n\t * the match. See {@link Autolinker.match.Match#getCssClassSuffixes} for\n\t * details.\n\t *\n\t * @return {String[]}\n\t */\n\tgetCssClassSuffixes() {\n\t\tlet cssClassSuffixes = super.getCssClassSuffixes(),\n\t\t    serviceName = this.getServiceName();\n\n\t\tif( serviceName ) {\n\t\t\tcssClassSuffixes.push( serviceName );\n\t\t}\n\t\treturn cssClassSuffixes;\n\t}\n\n}\n\nexport interface MentionMatchConfig extends MatchConfig {\n\tserviceName: MentionServices;\n\tmention: string;\n}"]} | {"version":3,"sources":["../src/match/mention-match.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,KAAK,EAAe,MAAM,SAAS,CAAA;AAG5C;;;;;;;GAOG;AACH;IAAkC,wCAAK;IAgBtC;;;;OAIG;IACH,sBAAa,GAAuB;QAApC,YACC,kBAAM,GAAG,CAAC,SAIV;QAzBD;;;;;WAKG;QACc,iBAAW,GAAoB,SAAS,CAAA,CAAC,gGAAgG;QAE1J;;;;WAIG;QACc,aAAO,GAAW,EAAE,CAAA,CAAC,gGAAgG;QAUrI,KAAI,CAAC,OAAO,GAAG,GAAG,CAAC,OAAO,CAAA;QAC1B,KAAI,CAAC,WAAW,GAAG,GAAG,CAAC,WAAW,CAAA;;IACnC,CAAC;IAED;;;;;OAKG;IACH,8BAAO,GAAP;QACC,OAAO,SAAS,CAAA;IACjB,CAAC;IAED;;;;OAIG;IACH,iCAAU,GAAV;QACC,OAAO,IAAI,CAAC,OAAO,CAAA;IACpB,CAAC;IAED;;;;;OAKG;IACH,qCAAc,GAAd;QACC,OAAO,IAAI,CAAC,WAAW,CAAA;IACxB,CAAC;IAED;;;;OAIG;IACH,oCAAa,GAAb;QACC,QAAQ,IAAI,CAAC,WAAW,EAAE;YACzB,KAAK,UAAU;gBACd,OAAO,sBAAsB,GAAG,IAAI,CAAC,OAAO,CAAA;YAC7C,KAAK,SAAS;gBACb,OAAO,sBAAsB,GAAG,IAAI,CAAC,OAAO,CAAA;YAC7C,KAAK,WAAW;gBACf,OAAO,wBAAwB,GAAG,IAAI,CAAC,OAAO,CAAA;YAC/C,KAAK,YAAY;gBAChB,OAAO,yBAAyB,GAAG,IAAI,CAAC,OAAO,CAAA;YAEhD;gBACC,uGAAuG;gBACvG,MAAM,IAAI,KAAK,CACd,4CAA4C,GAAG,IAAI,CAAC,WAAW,CAC/D,CAAA;SACF;IACF,CAAC;IAED;;;;OAIG;IACH,oCAAa,GAAb;QACC,OAAO,GAAG,GAAG,IAAI,CAAC,OAAO,CAAA;IAC1B,CAAC;IAED;;;;;;OAMG;IACH,0CAAmB,GAAnB;QACC,IAAI,gBAAgB,GAAG,iBAAM,mBAAmB,WAAE,EACjD,WAAW,GAAG,IAAI,CAAC,cAAc,EAAE,CAAA;QAEpC,IAAI,WAAW,EAAE;YAChB,gBAAgB,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;SAClC;QACD,OAAO,gBAAgB,CAAA;IACxB,CAAC;IACF,mBAAC;AAAD,CA1GA,AA0GC,CA1GiC,KAAK,GA0GtC","file":"mention-match.js","sourcesContent":["import { Match, MatchConfig } from './match'\nimport { MentionServices } from '../autolinker'\n\n/**\n * @class Autolinker.match.Mention\n * @extends Autolinker.match.Match\n *\n * Represents a Mention match found in an input string which should be Autolinked.\n *\n * See this class's superclass ({@link Autolinker.match.Match}) for more details.\n */\nexport class MentionMatch extends Match {\n\t/**\n\t * @cfg {String} serviceName\n\t *\n\t * The service to point mention matches to. See {@link Autolinker#mention}\n\t * for available values.\n\t */\n\tprivate readonly serviceName: MentionServices = 'twitter' // default value just to get the above doc comment in the ES5 output and documentation generator\n\n\t/**\n\t * @cfg {String} mention (required)\n\t *\n\t * The Mention that was matched, without the '@' character.\n\t */\n\tprivate readonly mention: string = '' // default value just to get the above doc comment in the ES5 output and documentation generator\n\n\t/**\n\t * @method constructor\n\t * @param {Object} cfg The configuration properties for the Match\n\t *   instance, specified in an Object (map).\n\t */\n\tconstructor (cfg: MentionMatchConfig) {\n\t\tsuper(cfg)\n\n\t\tthis.mention = cfg.mention\n\t\tthis.serviceName = cfg.serviceName\n\t}\n\n\t/**\n\t * Returns a string name for the type of match that this class represents.\n\t * For the case of MentionMatch, returns 'mention'.\n\t *\n\t * @return {String}\n\t */\n\tgetType () {\n\t\treturn 'mention'\n\t}\n\n\t/**\n\t * Returns the mention, without the '@' character.\n\t *\n\t * @return {String}\n\t */\n\tgetMention () {\n\t\treturn this.mention\n\t}\n\n\t/**\n\t * Returns the configured {@link #serviceName} to point the mention to.\n\t * Ex: 'instagram', 'twitter', 'soundcloud'.\n\t *\n\t * @return {String}\n\t */\n\tgetServiceName () {\n\t\treturn this.serviceName\n\t}\n\n\t/**\n\t * Returns the anchor href that should be generated for the match.\n\t *\n\t * @return {String}\n\t */\n\tgetAnchorHref () {\n\t\tswitch (this.serviceName) {\n\t\t\tcase 'mastodon':\n\t\t\t\treturn 'https://example.com/' + this.mention\n\t\t\tcase 'twitter':\n\t\t\t\treturn 'https://twitter.com/' + this.mention\n\t\t\tcase 'instagram':\n\t\t\t\treturn 'https://instagram.com/' + this.mention\n\t\t\tcase 'soundcloud':\n\t\t\t\treturn 'https://soundcloud.com/' + this.mention\n\n\t\t\tdefault:\n\t\t\t\t// Shouldn't happen because Autolinker's constructor should block any invalid values, but just in case.\n\t\t\t\tthrow new Error(\n\t\t\t\t\t'Unknown service name to point mention to: ' + this.serviceName\n\t\t\t\t)\n\t\t}\n\t}\n\n\t/**\n\t * Returns the anchor text that should be generated for the match.\n\t *\n\t * @return {String}\n\t */\n\tgetAnchorText () {\n\t\treturn '@' + this.mention\n\t}\n\n\t/**\n\t * Returns the CSS class suffixes that should be used on a tag built with\n\t * the match. See {@link Autolinker.match.Match#getCssClassSuffixes} for\n\t * details.\n\t *\n\t * @return {String[]}\n\t */\n\tgetCssClassSuffixes () {\n\t\tlet cssClassSuffixes = super.getCssClassSuffixes(),\n\t\t\tserviceName = this.getServiceName()\n\n\t\tif (serviceName) {\n\t\t\tcssClassSuffixes.push(serviceName)\n\t\t}\n\t\treturn cssClassSuffixes\n\t}\n}\n\nexport interface MentionMatchConfig extends MatchConfig {\n\tserviceName: MentionServices\n\tmention: string\n}\n"]} | ||||||
| @@ -7,7 +7,7 @@ import { MentionMatch } from "../match/mention-match"; | |||||||
| // called multiple times, thus instantiating MentionMatcher and its RegExp  | // called multiple times, thus instantiating MentionMatcher and its RegExp  | ||||||
| // objects each time (which is very expensive - see https://github.com/gregjacobs/Autolinker.js/issues/314).  | // objects each time (which is very expensive - see https://github.com/gregjacobs/Autolinker.js/issues/314).  | ||||||
| // See descriptions of the properties where they are used for details about them | // See descriptions of the properties where they are used for details about them | ||||||
| var mastodonRegex = new RegExp("@[_@." + alphaNumericAndMarksCharsStr + "]{1,}(?![_" + alphaNumericAndMarksCharsStr + "])", 'g'); | var mastodonRegex = new RegExp("@[_" + alphaNumericAndMarksCharsStr + "]{1,}[@]?[_." + alphaNumericAndMarksCharsStr + "]{0,}(?![_" + alphaNumericAndMarksCharsStr + "])", 'g'); | ||||||
| var twitterRegex = new RegExp("@[_" + alphaNumericAndMarksCharsStr + "]{1,50}(?![_" + alphaNumericAndMarksCharsStr + "])", 'g'); // lookahead used to make sure we don't match something above 50 characters | var twitterRegex = new RegExp("@[_" + alphaNumericAndMarksCharsStr + "]{1,50}(?![_" + alphaNumericAndMarksCharsStr + "])", 'g'); // lookahead used to make sure we don't match something above 50 characters | ||||||
| var instagramRegex = new RegExp("@[_." + alphaNumericAndMarksCharsStr + "]{1,30}(?![_" + alphaNumericAndMarksCharsStr + "])", 'g'); // lookahead used to make sure we don't match something above 30 characters | var instagramRegex = new RegExp("@[_." + alphaNumericAndMarksCharsStr + "]{1,30}(?![_" + alphaNumericAndMarksCharsStr + "])", 'g'); // lookahead used to make sure we don't match something above 30 characters | ||||||
| var soundcloudRegex = new RegExp("@[-_." + alphaNumericAndMarksCharsStr + "]{1,50}(?![-_" + alphaNumericAndMarksCharsStr + "])", 'g'); // lookahead used to make sure we don't match something above 50 characters | var soundcloudRegex = new RegExp("@[-_." + alphaNumericAndMarksCharsStr + "]{1,50}(?![-_" + alphaNumericAndMarksCharsStr + "])", 'g'); // lookahead used to make sure we don't match something above 50 characters | ||||||
|   | |||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -1,6 +1,7 @@ | |||||||
| import { Feather } from '@expo/vector-icons' | import { Feather } from '@expo/vector-icons' | ||||||
| import React, { useCallback, useEffect, useState } from 'react' | import React, { useCallback, useEffect, useMemo, useState } from 'react' | ||||||
| import { | import { | ||||||
|  |   ActivityIndicator, | ||||||
|   Alert, |   Alert, | ||||||
|   Keyboard, |   Keyboard, | ||||||
|   KeyboardAvoidingView, |   KeyboardAvoidingView, | ||||||
| @@ -13,11 +14,67 @@ import { | |||||||
| import { createNativeStackNavigator } from 'react-native-screens/native-stack' | import { createNativeStackNavigator } from 'react-native-screens/native-stack' | ||||||
| import Autolinker from 'src/modules/autolinker' | import Autolinker from 'src/modules/autolinker' | ||||||
| import { useNavigation } from '@react-navigation/native' | import { useNavigation } from '@react-navigation/native' | ||||||
|  | import { debounce, differenceWith, isEqual } from 'lodash' | ||||||
|  | import { searchFetch } from '../common/searchFetch' | ||||||
|  | import { useQuery } from 'react-query' | ||||||
|  | import { FlatList } from 'react-native-gesture-handler' | ||||||
|  |  | ||||||
| const Stack = createNativeStackNavigator() | const Stack = createNativeStackNavigator() | ||||||
|  |  | ||||||
|  | const Suggestion = React.memo(({ item, index }) => { | ||||||
|  |   return ( | ||||||
|  |     <View key={index}> | ||||||
|  |       <Text>{item.acct ? item.acct : item.name}</Text> | ||||||
|  |     </View> | ||||||
|  |   ) | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | const Suggestions = ({ | ||||||
|  |   type, | ||||||
|  |   text | ||||||
|  | }: { | ||||||
|  |   type: 'mention' | 'hashtag' | ||||||
|  |   text: string | ||||||
|  | }) => { | ||||||
|  |   const { status, data } = useQuery( | ||||||
|  |     [ | ||||||
|  |       'Search', | ||||||
|  |       { type: type === 'mention' ? 'accounts' : 'hashtags', term: text } | ||||||
|  |     ], | ||||||
|  |     searchFetch, | ||||||
|  |     { retry: false } | ||||||
|  |   ) | ||||||
|  |  | ||||||
|  |   let content | ||||||
|  |   switch (status) { | ||||||
|  |     case 'success': | ||||||
|  |       content = data[type === 'mention' ? 'accounts' : 'hashtags'].length ? ( | ||||||
|  |         <FlatList | ||||||
|  |           data={data[type === 'mention' ? 'accounts' : 'hashtags']} | ||||||
|  |           renderItem={({ item, index, separators }) => ( | ||||||
|  |             <Suggestion item={item} index={index} /> | ||||||
|  |           )} | ||||||
|  |         /> | ||||||
|  |       ) : ( | ||||||
|  |         <Text>空无一物</Text> | ||||||
|  |       ) | ||||||
|  |       break | ||||||
|  |     case 'loading': | ||||||
|  |       content = <ActivityIndicator /> | ||||||
|  |       break | ||||||
|  |     case 'error': | ||||||
|  |       content = <Text>搜索错误</Text> | ||||||
|  |       break | ||||||
|  |     default: | ||||||
|  |       content = <></> | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return content | ||||||
|  | } | ||||||
|  |  | ||||||
| const PostTootMain = () => { | const PostTootMain = () => { | ||||||
|   const [viewHeight, setViewHeight] = useState(0) |   const [viewHeight, setViewHeight] = useState(0) | ||||||
|  |   const [contentHeight, setContentHeight] = useState(0) | ||||||
|   const [keyboardHeight, setKeyboardHeight] = useState(0) |   const [keyboardHeight, setKeyboardHeight] = useState(0) | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     Keyboard.addListener('keyboardDidShow', _keyboardDidShow) |     Keyboard.addListener('keyboardDidShow', _keyboardDidShow) | ||||||
| @@ -39,33 +96,50 @@ const PostTootMain = () => { | |||||||
|  |  | ||||||
|   const [charCount, setCharCount] = useState(0) |   const [charCount, setCharCount] = useState(0) | ||||||
|   const [formattedText, setFormattedText] = useState<React.ReactNode>() |   const [formattedText, setFormattedText] = useState<React.ReactNode>() | ||||||
|   let prevTags = [] |   const [suggestionsShown, setSuggestionsShown] = useState({ | ||||||
|  |     display: false, | ||||||
|  |     tag: undefined | ||||||
|  |   }) | ||||||
|  |   const debouncedSuggestions = useCallback( | ||||||
|  |     debounce(tag => setSuggestionsShown({ display: true, tag }), 300), | ||||||
|  |     [] | ||||||
|  |   ) | ||||||
|  |   let prevTags: { type: 'url' | 'mention' | 'hashtag'; text: string }[] = [] | ||||||
|   const onChangeText = useCallback(content => { |   const onChangeText = useCallback(content => { | ||||||
|     const tags: string[] = [] |     const tags: { type: 'url' | 'mention' | 'hashtag'; text: string }[] = [] | ||||||
|     Autolinker.link(content, { |     Autolinker.link(content, { | ||||||
|       email: true, |       email: false, | ||||||
|       phone: false, |       phone: false, | ||||||
|       mention: 'mastodon', |       mention: 'mastodon', | ||||||
|       hashtag: 'twitter', |       hashtag: 'twitter', | ||||||
|       replaceFn: props => { |       replaceFn: props => { | ||||||
|         const tag = props.getMatchedText() |         // @ts-ignore | ||||||
|         tags.push(tag) |         tags.push({ type: props.getType(), text: props.getMatchedText() }) | ||||||
|         return tag |         return | ||||||
|       } |       } | ||||||
|     }) |     }) | ||||||
|  |  | ||||||
|  |     const changedTag = differenceWith(prevTags, tags, isEqual) | ||||||
|  |     if (changedTag.length) { | ||||||
|  |       if (changedTag[0].type !== 'url') { | ||||||
|  |         debouncedSuggestions(changedTag[0]) | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       setSuggestionsShown({ display: false, tag: undefined }) | ||||||
|  |     } | ||||||
|     prevTags = tags |     prevTags = tags | ||||||
|  |  | ||||||
|     let _content = content |     let _content = content | ||||||
|     const children = [] |     const children = [] | ||||||
|     tags.forEach(tag => { |     tags.forEach(tag => { | ||||||
|       const parts = _content.split(tag) |       const parts = _content.split(tag.text) | ||||||
|       children.push(parts.shift()) |       children.push(parts.shift()) | ||||||
|       children.push( |       children.push( | ||||||
|         <Text style={{ color: 'red' }} key={Math.random()}> |         <Text style={{ color: 'red' }} key={Math.random()}> | ||||||
|           {tag} |           {tag.text} | ||||||
|         </Text> |         </Text> | ||||||
|       ) |       ) | ||||||
|       _content = parts.join(tag) |       _content = parts.join(tag.text) | ||||||
|     }) |     }) | ||||||
|     children.push(_content) |     children.push(_content) | ||||||
|  |  | ||||||
| @@ -80,17 +154,39 @@ const PostTootMain = () => { | |||||||
|     > |     > | ||||||
|       <View style={{ height: viewHeight - keyboardHeight }}> |       <View style={{ height: viewHeight - keyboardHeight }}> | ||||||
|         <TextInput |         <TextInput | ||||||
|           style={styles.textInput} |           style={[ | ||||||
|  |             styles.textInput, | ||||||
|  |             { | ||||||
|  |               flex: suggestionsShown.display ? 0 : 1, | ||||||
|  |               minHeight: contentHeight + 14 | ||||||
|  |             } | ||||||
|  |           ]} | ||||||
|           autoCapitalize='none' |           autoCapitalize='none' | ||||||
|  |           autoCorrect={false} | ||||||
|           autoFocus |           autoFocus | ||||||
|           enablesReturnKeyAutomatically |           enablesReturnKeyAutomatically | ||||||
|           multiline |           multiline | ||||||
|           placeholder='想说点什么' |           placeholder='想说点什么' | ||||||
|           onChangeText={onChangeText} |           onChangeText={onChangeText} | ||||||
|  |           onContentSizeChange={({ nativeEvent }) => { | ||||||
|  |             setContentHeight(nativeEvent.contentSize.height) | ||||||
|  |           }} | ||||||
|           scrollEnabled |           scrollEnabled | ||||||
|         > |         > | ||||||
|           <Text>{formattedText}</Text> |           <Text>{formattedText}</Text> | ||||||
|         </TextInput> |         </TextInput> | ||||||
|  |         {suggestionsShown.display ? ( | ||||||
|  |           <View | ||||||
|  |             style={[ | ||||||
|  |               styles.suggestions | ||||||
|  |               // { height: viewHeight - contentHeight - keyboardHeight - 44 } | ||||||
|  |             ]} | ||||||
|  |           > | ||||||
|  |             <Suggestions {...suggestionsShown.tag} /> | ||||||
|  |           </View> | ||||||
|  |         ) : ( | ||||||
|  |           <></> | ||||||
|  |         )} | ||||||
|         <Pressable style={styles.additions} onPress={() => Keyboard.dismiss()}> |         <Pressable style={styles.additions} onPress={() => Keyboard.dismiss()}> | ||||||
|           <Feather name='paperclip' size={24} /> |           <Feather name='paperclip' size={24} /> | ||||||
|           <Feather name='bar-chart-2' size={24} /> |           <Feather name='bar-chart-2' size={24} /> | ||||||
| @@ -144,9 +240,12 @@ const styles = StyleSheet.create({ | |||||||
|     flex: 1 |     flex: 1 | ||||||
|   }, |   }, | ||||||
|   textInput: { |   textInput: { | ||||||
|     flex: 1, |  | ||||||
|     backgroundColor: 'gray' |     backgroundColor: 'gray' | ||||||
|   }, |   }, | ||||||
|  |   suggestions: { | ||||||
|  |     flex: 1, | ||||||
|  |     backgroundColor: 'lightyellow' | ||||||
|  |   }, | ||||||
|   additions: { |   additions: { | ||||||
|     height: 44, |     height: 44, | ||||||
|     backgroundColor: 'red', |     backgroundColor: 'red', | ||||||
|   | |||||||
							
								
								
									
										23
									
								
								src/stacks/common/searchFetch.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/stacks/common/searchFetch.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | import client from 'src/api/client' | ||||||
|  |  | ||||||
|  | export const searchFetch = async ( | ||||||
|  |   {} = {}, | ||||||
|  |   { | ||||||
|  |     type, | ||||||
|  |     term, | ||||||
|  |     limit = 20 | ||||||
|  |   }: { | ||||||
|  |     type: 'accounts' | 'hashtags' | 'statuses' | ||||||
|  |     term: string | ||||||
|  |     limit?: number | ||||||
|  |   } | ||||||
|  | ) => { | ||||||
|  |   const res = await client({ | ||||||
|  |     version: 'v2', | ||||||
|  |     method: 'get', | ||||||
|  |     instance: 'local', | ||||||
|  |     endpoint: 'search', | ||||||
|  |     query: { type, q: term, limit } | ||||||
|  |   }) | ||||||
|  |   return Promise.resolve(res.body) | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user