1- import { useCallback , useRef } from 'react' ;
2- import {
3- ActionSheetIOS ,
4- Alert ,
5- Animated ,
6- Pressable ,
7- StyleSheet ,
8- View ,
9- findNodeHandle ,
10- } from 'react-native' ;
1+ import { useCallback } from 'react' ;
2+ import { Alert , StyleSheet , View , useWindowDimensions } from 'react-native' ;
113
4+ import {
5+ ContextMenu ,
6+ Button ,
7+ Divider ,
8+ Host ,
9+ RNHostView ,
10+ } from '@expo/ui/swift-ui' ;
1211import { useRouter } from 'expo-router' ;
1312import { TouchableOpacity } from 'react-native-gesture-handler' ;
1413
1514import * as Haptics from 'expo-haptics' ;
1615
17- import { format } from 'date-fns/format' ;
18-
1916import useStore from '../hooks/useStore' ;
2017import useTheme from '../hooks/useTheme' ;
21- import useViewport from '../hooks/useViewport' ;
2218
2319import openBrowser from '../utils/openBrowser' ;
2420import openShare from '../utils/openShare' ;
@@ -54,9 +50,8 @@ export default function Comment({
5450} ) {
5551 const router = useRouter ( ) ;
5652 const { colors } = useTheme ( ) ;
57- const { exceedsReadableWidth } = useViewport ( ) ;
53+ const { height : windowHeight } = useWindowDimensions ( ) ;
5854 const { id, user, time, content, deleted, dead, comments } = item ;
59- const datetime = new Date ( time * 1000 ) ;
6055 const currentOP = useStore (
6156 useCallback (
6257 ( state ) =>
@@ -71,190 +66,168 @@ export default function Comment({
7166 if ( dead || ( deleted && ! comments . length ) ) return null ;
7267 const hnURL = `https://news.ycombinator.com/item?id=${ id } ` ;
7368
74- const scaleAnim = useRef ( new Animated . Value ( 1 ) ) . current ;
75- const bobble = useCallback ( ( ) => {
76- Animated . sequence ( [
77- Animated . timing ( scaleAnim , {
78- toValue : 0.97 ,
79- duration : 150 ,
80- useNativeDriver : true ,
81- } ) ,
82- Animated . timing ( scaleAnim , {
83- toValue : 1 ,
84- duration : 150 ,
85- useNativeDriver : true ,
86- } ) ,
87- ] ) . start ( ) ;
88- } , [ scaleAnim ] ) ;
89-
90- const commentRef = useRef ( ) ;
91-
92- const options = [
93- {
94- text : 'View profile' ,
95- action : ( ) => {
96- router . push ( `/user/${ user } ` ) ;
97- } ,
98- } ,
99- ! settingsInteractions && {
100- text : 'View comment on HN web site' ,
101- action : ( ) => {
102- openBrowser ( hnURL ) ;
103- } ,
104- } ,
105- settingsInteractions && {
106- text : 'Upvote comment on HN' ,
107- action : ( ) => {
108- const jsKey = `web-view-${ Date . now ( ) } ` ;
109- useStore . getState ( ) . setRouteInjectedJS (
110- jsKey ,
111- `
112- try {
113- document.getElementById('up_${ id } ').click();
114- } catch (e) {}
115- true;
116- ` ,
117- ) ;
118- router . push ( {
119- pathname : '/web-view' ,
120- params : {
121- url : hnURL ,
122- jsKey,
123- } ,
124- } ) ;
125- } ,
126- } ,
127- settingsInteractions && {
128- text : 'View or Reply comment on HN' ,
129- action : ( ) => {
130- router . push ( {
131- pathname : '/web-view' ,
132- params : {
133- url : `https://news.ycombinator.com/reply?id=${ id } &goto=${ encodeURIComponent (
134- `item?id=${ id } ` ,
135- ) } `,
136- } ,
137- } ) ;
138- } ,
139- } ,
140- ! disableViewThread && {
141- text : "View comment's thread" ,
142- action : ( ) => {
143- router . push ( `/thread/${ storyID } /${ id } ` ) ;
144- } ,
145- } ,
146- ! disableViewThread && {
147- text : 'Share as Image…' ,
148- action : ( ) => {
149- router . push ( `/thread/${ storyID } /${ id } ?tab=share` ) ;
150- } ,
151- } ,
152- {
153- text : 'Share comment…' ,
154- action : ( ) => {
155- openShare ( { url : hnURL } ) ;
156- } ,
157- } ,
158- __DEV__ && {
159- text : '🚧 View HTML' ,
160- action : ( ) => {
161- Alert . alert ( 'Comment HTML' , content ) ;
162- } ,
163- } ,
164- {
165- text : 'Cancel' ,
166- cancel : true ,
167- } ,
168- ] . filter ( Boolean ) ;
169-
170- const onShowActionSheet = useCallback ( ( ) => {
171- ActionSheetIOS . showActionSheetWithOptions (
172- {
173- title : `Comment by ${ user } ` ,
174- message : `${ format ( datetime , 'EEEE, d LLLL yyyy, h:mm a' ) } \n${ hnURL } ` ,
175- options : options . map ( ( o ) => o . text ) ,
176- cancelButtonIndex : options . findIndex ( ( o ) => o . cancel ) ,
177- anchor : exceedsReadableWidth
178- ? findNodeHandle ( commentRef . current )
179- : undefined ,
180- } ,
181- ( index ) => {
182- options [ index ] . action ?. ( ) ;
183- } ,
184- ) ;
185- } , [ options , user , datetime , hnURL ] ) ;
186-
187- return (
188- < Pressable
189- ref = { commentRef }
190- onLongPress = { ( ) => {
191- bobble ( ) ;
192- Haptics . selectionAsync ( ) ;
193- onShowActionSheet ( ) ;
194- } }
195- >
196- < Animated . View style = { { transform : [ { scale : scaleAnim } ] } } >
197- { ! deleted && (
198- < View style = { styles . metadata } >
199- < Text
200- size = { significant ? 'body' : 'subhead' }
201- bold = { ! significant }
202- bolder = { significant }
203- style = { {
204- color : colors . red ,
205- flexShrink : 1 ,
206- } }
207- numberOfLines = { 1 }
69+ const commentContent = (
70+ < View >
71+ { ! deleted && (
72+ < View style = { styles . metadata } >
73+ < Text
74+ size = { significant ? 'body' : 'subhead' }
75+ bold = { ! significant }
76+ bolder = { significant }
77+ style = { {
78+ color : colors . red ,
79+ flexShrink : 1 ,
80+ } }
81+ numberOfLines = { 1 }
82+ onPress = { ( ) => {
83+ router . push ( `/user/${ user } ` ) ;
84+ } }
85+ >
86+ { user }
87+ </ Text >
88+ { user === currentOP && (
89+ < TouchableOpacity
90+ style = { [
91+ styles . opBox ,
92+ {
93+ backgroundColor : colors . red ,
94+ } ,
95+ ] }
20896 onPress = { ( ) => {
209- router . push ( `/user/${ user } ` ) ;
97+ Alert . alert (
98+ 'What does OP mean?' ,
99+ 'OP is short for Original Poster — the person who posted this story.' ,
100+ ) ;
101+ } }
102+ hitSlop = { {
103+ top : 10 ,
104+ right : 10 ,
105+ bottom : 20 ,
106+ left : 10 ,
210107 } }
211108 >
212- { user }
213- </ Text >
214- { user === currentOP && (
215- < TouchableOpacity
216- style = { [
217- styles . opBox ,
218- {
219- backgroundColor : colors . red ,
220- } ,
221- ] }
222- onPress = { ( ) => {
223- Alert . alert (
224- 'What does OP mean?' ,
225- 'OP is short for Original Poster — the person who posted this story.' ,
226- ) ;
227- } }
228- hitSlop = { {
229- top : 10 ,
230- right : 10 ,
231- bottom : 20 ,
232- left : 10 ,
233- } }
234- >
235- < Text
236- size = { significant ? 'footnote' : 'caption2' }
237- style = { styles . op }
238- >
239- OP
240- </ Text >
241- </ TouchableOpacity >
242- ) }
243- < Text size = { significant ? 'body' : 'subhead' } type = "insignificant" >
244- { ' ' }
245- •{ ' ' }
246109 < Text
247- size = { significant ? 'body' : 'subhead' }
248- type = "insignificant"
249- onPress = { onShowActionSheet }
110+ size = { significant ? 'footnote' : 'caption2' }
111+ style = { styles . op }
250112 >
251- < TimeAgo time = { datetime } />
113+ OP
252114 </ Text >
115+ </ TouchableOpacity >
116+ ) }
117+ < Text size = { significant ? 'body' : 'subhead' } type = "insignificant" >
118+ { ' ' }
119+ •{ ' ' }
120+ < Text size = { significant ? 'body' : 'subhead' } type = "insignificant" >
121+ < TimeAgo time = { new Date ( time * 1000 ) } />
253122 </ Text >
254- </ View >
255- ) }
256- < HTMLView2 html = { content } fontSize = { significant ? 17 : undefined } />
257- </ Animated . View >
258- </ Pressable >
123+ </ Text >
124+ </ View >
125+ ) }
126+ < HTMLView2 html = { content } fontSize = { significant ? 17 : undefined } />
127+ </ View >
128+ ) ;
129+
130+ return (
131+ < Host matchContents >
132+ < ContextMenu >
133+ < ContextMenu . Trigger >
134+ < RNHostView matchContents > { commentContent } </ RNHostView >
135+ </ ContextMenu . Trigger >
136+ < ContextMenu . Preview >
137+ < RNHostView matchContents >
138+ < View
139+ style = { {
140+ padding : 15 ,
141+ maxHeight : Math . max ( 100 , windowHeight * 0.33 ) ,
142+ } }
143+ >
144+ { commentContent }
145+ </ View >
146+ </ RNHostView >
147+ </ ContextMenu . Preview >
148+ < ContextMenu . Items >
149+ < Button
150+ label = "View profile"
151+ systemImage = "person"
152+ onPress = { ( ) => router . push ( `/user/${ user } ` ) }
153+ />
154+ < Divider />
155+ { ! settingsInteractions && (
156+ < Button
157+ label = "View comment on HN web site"
158+ systemImage = "safari"
159+ onPress = { ( ) => openBrowser ( hnURL ) }
160+ />
161+ ) }
162+ { settingsInteractions && (
163+ < Button
164+ label = "Upvote comment on HN"
165+ systemImage = "arrowtriangle.up.fill"
166+ onPress = { ( ) => {
167+ Haptics . selectionAsync ( ) ;
168+ const jsKey = `web-view-${ Date . now ( ) } ` ;
169+ useStore . getState ( ) . setRouteInjectedJS (
170+ jsKey ,
171+ `
172+ try {
173+ document.getElementById('up_${ id } ').click();
174+ } catch (e) {}
175+ true;
176+ ` ,
177+ ) ;
178+ router . push ( {
179+ pathname : '/web-view' ,
180+ params : { url : hnURL , jsKey } ,
181+ } ) ;
182+ } }
183+ />
184+ ) }
185+ { settingsInteractions && (
186+ < Button
187+ label = "View or Reply comment on HN"
188+ systemImage = "arrowshape.turn.up.left"
189+ onPress = { ( ) =>
190+ router . push ( {
191+ pathname : '/web-view' ,
192+ params : {
193+ url : `https://news.ycombinator.com/reply?id=${ id } &goto=${ encodeURIComponent (
194+ `item?id=${ id } ` ,
195+ ) } `,
196+ } ,
197+ } )
198+ }
199+ />
200+ ) }
201+ < Divider />
202+ { ! disableViewThread && (
203+ < Button
204+ label = "View comment's thread"
205+ systemImage = "text.bubble"
206+ onPress = { ( ) => router . push ( `/thread/${ storyID } /${ id } ` ) }
207+ />
208+ ) }
209+ { ! disableViewThread && (
210+ < Button
211+ label = "Share as Image…"
212+ systemImage = "photo"
213+ onPress = { ( ) => router . push ( `/thread/${ storyID } /${ id } ?tab=share` ) }
214+ />
215+ ) }
216+ < Button
217+ label = "Share comment…"
218+ systemImage = "square.and.arrow.up"
219+ onPress = { ( ) => openShare ( { url : hnURL } ) }
220+ />
221+ { __DEV__ && < Divider /> }
222+ { __DEV__ && (
223+ < Button
224+ label = "View HTML"
225+ systemImage = "chevron.left.forwardslash.chevron.right"
226+ onPress = { ( ) => Alert . alert ( 'Comment HTML' , content ) }
227+ />
228+ ) }
229+ </ ContextMenu . Items >
230+ </ ContextMenu >
231+ </ Host >
259232 ) ;
260233}
0 commit comments