Commit 34d18b003d73045d55dd58e8a0c8dcbc0bb47db5
ux: implement RawDatabase screen accessible from drawer
Andre Staltz committed on 7/19/2018, 6:26:01 PMParent: 2457384782d72e2f309e722376a55bd6578b86f0
Files changed
src/app/components/messages/ShortRawMessage.ts | ||
---|---|---|
@@ -1,0 +1,152 @@ | ||
1 | +/** | |
2 | + * MMMMM is a mobile app for Secure Scuttlebutt networks | |
3 | + * | |
4 | + * Copyright (C) 2017 Andre 'Staltz' Medeiros | |
5 | + * | |
6 | + * This program is free software: you can redistribute it and/or modify | |
7 | + * it under the terms of the GNU General Public License as published by | |
8 | + * the Free Software Foundation, either version 3 of the License, or | |
9 | + * (at your option) any later version. | |
10 | + * | |
11 | + * This program is distributed in the hope that it will be useful, | |
12 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
13 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
14 | + * GNU General Public License for more details. | |
15 | + * | |
16 | + * You should have received a copy of the GNU General Public License | |
17 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. | |
18 | + */ | |
19 | + | |
20 | +import {PureComponent} from 'react'; | |
21 | +import {h} from '@cycle/react'; | |
22 | +import { | |
23 | + Text, | |
24 | + View, | |
25 | + TouchableNativeFeedback, | |
26 | + Image, | |
27 | + StyleSheet, | |
28 | +} from 'react-native'; | |
29 | +import MessageContainer from './MessageContainer'; | |
30 | +import HumanTime from 'react-human-time'; | |
31 | +import {MsgId, Msg, PostContent} from 'ssb-typescript'; | |
32 | +import {authorName} from '../../../ssb/from-ssb'; | |
33 | +import {Dimensions} from '../../global-styles/dimens'; | |
34 | +import {Palette} from '../../global-styles/palette'; | |
35 | +import {Typography} from '../../global-styles/typography'; | |
36 | +import {isPrivate} from 'ssb-typescript/utils'; | |
37 | + | |
38 | +export const styles = StyleSheet.create({ | |
39 | + row: { | |
40 | + flexDirection: 'row', | |
41 | + flex: 1, | |
42 | + }, | |
43 | + | |
44 | + avatarContainer: { | |
45 | + height: Dimensions.avatarSizeNormal, | |
46 | + width: Dimensions.avatarSizeNormal, | |
47 | + borderRadius: Dimensions.avatarBorderRadius, | |
48 | + backgroundColor: Palette.indigo1, | |
49 | + marginRight: Dimensions.horizontalSpaceSmall, | |
50 | + }, | |
51 | + | |
52 | + avatar: { | |
53 | + borderRadius: Dimensions.avatarBorderRadius, | |
54 | + position: 'absolute', | |
55 | + top: 0, | |
56 | + left: 0, | |
57 | + right: 0, | |
58 | + bottom: 0, | |
59 | + }, | |
60 | + | |
61 | + authorColumn: { | |
62 | + flexDirection: 'column', | |
63 | + flex: 1, | |
64 | + alignItems: 'flex-start', | |
65 | + justifyContent: 'space-around', | |
66 | + }, | |
67 | + | |
68 | + authorName: { | |
69 | + fontSize: Typography.fontSizeNormal, | |
70 | + fontWeight: 'bold', | |
71 | + fontFamily: Typography.fontFamilyReadableText, | |
72 | + color: Palette.brand.text, | |
73 | + minWidth: 120, | |
74 | + }, | |
75 | + | |
76 | + msgType: { | |
77 | + fontSize: Typography.fontSizeSmall, | |
78 | + fontFamily: Typography.fontFamilyMonospace, | |
79 | + backgroundColor: Palette.brand.darkVoidBackground, | |
80 | + color: Palette.brand.darkText, | |
81 | + }, | |
82 | + | |
83 | + timestamp: { | |
84 | + fontSize: Typography.fontSizeSmall, | |
85 | + fontFamily: Typography.fontFamilyReadableText, | |
86 | + color: Palette.brand.textWeak, | |
87 | + }, | |
88 | +}); | |
89 | + | |
90 | +export type Props = { | |
91 | + msg: Msg; | |
92 | + name: string | null; | |
93 | + imageUrl: string | null; | |
94 | + onPress?: (ev: {msgId: MsgId}) => void; | |
95 | +}; | |
96 | + | |
97 | +export default class RawMessage extends PureComponent<Props> { | |
98 | + private _onPress() { | |
99 | + const {onPress, msg} = this.props; | |
100 | + if (onPress) { | |
101 | + onPress({msgId: msg.key}); | |
102 | + } | |
103 | + } | |
104 | + | |
105 | + public render() { | |
106 | + const {msg, name, imageUrl} = this.props; | |
107 | + const avatarUrl = {uri: imageUrl || undefined}; | |
108 | + const touchableProps = { | |
109 | + background: TouchableNativeFeedback.SelectableBackground(), | |
110 | + onPress: () => this._onPress(), | |
111 | + }; | |
112 | + | |
113 | + const authorNameText = h( | |
114 | + Text, | |
115 | + { | |
116 | + numberOfLines: 1, | |
117 | + ellipsizeMode: 'middle', | |
118 | + style: styles.authorName, | |
119 | + }, | |
120 | + authorName(name, msg), | |
121 | + ); | |
122 | + | |
123 | + const msgTypeText = h( | |
124 | + Text, | |
125 | + {style: styles.msgType}, | |
126 | + isPrivate(msg) | |
127 | + ? 'encrypted' | |
128 | + : 'type: ' + (msg.value.content as PostContent).type, | |
129 | + ); | |
130 | + | |
131 | + const timestampText = h(Text, {style: styles.timestamp}, [ | |
132 | + h(HumanTime as any, {time: msg.value.timestamp}), | |
133 | + ]); | |
134 | + | |
135 | + return h(TouchableNativeFeedback, touchableProps, [ | |
136 | + h(MessageContainer, [ | |
137 | + h(View, {style: styles.row}, [ | |
138 | + h(View, {style: styles.avatarContainer}, [ | |
139 | + h(Image, { | |
140 | + style: styles.avatar, | |
141 | + source: avatarUrl, | |
142 | + }), | |
143 | + ]), | |
144 | + h(View, {style: styles.authorColumn}, [ | |
145 | + authorNameText, | |
146 | + h(Text, [timestampText, ' ' as any, msgTypeText]), | |
147 | + ]), | |
148 | + ]), | |
149 | + ]), | |
150 | + ]); | |
151 | + } | |
152 | +} |
src/app/components/RawFeed.ts | ||
---|---|---|
@@ -1,0 +1,76 @@ | ||
1 | +/** | |
2 | + * MMMMM is a mobile app for Secure Scuttlebutt networks | |
3 | + * | |
4 | + * Copyright (C) 2017 Andre 'Staltz' Medeiros | |
5 | + * | |
6 | + * This program is free software: you can redistribute it and/or modify | |
7 | + * it under the terms of the GNU General Public License as published by | |
8 | + * the Free Software Foundation, either version 3 of the License, or | |
9 | + * (at your option) any later version. | |
10 | + * | |
11 | + * This program is distributed in the hope that it will be useful, | |
12 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
13 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
14 | + * GNU General Public License for more details. | |
15 | + * | |
16 | + * You should have received a copy of the GNU General Public License | |
17 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. | |
18 | + */ | |
19 | + | |
20 | +import {PureComponent} from 'react'; | |
21 | +import {View, StyleSheet} from 'react-native'; | |
22 | +import {h} from '@cycle/react'; | |
23 | +import ShortRawMessage from './messages/ShortRawMessage'; | |
24 | +import {Palette} from '../global-styles/palette'; | |
25 | +import {GetReadable, MsgAndExtras} from '../drivers/ssb'; | |
26 | +import PullFlatList, {PullFlatListProps} from 'pull-flat-list'; | |
27 | +import {withMutantProps} from 'react-mutant-hoc'; | |
28 | + | |
29 | +const ShortRawMessageM = withMutantProps(ShortRawMessage, 'name', 'imageUrl'); | |
30 | + | |
31 | +export const styles = StyleSheet.create({ | |
32 | + container: { | |
33 | + alignSelf: 'stretch', | |
34 | + flex: 1, | |
35 | + }, | |
36 | + | |
37 | + itemSeparator: { | |
38 | + backgroundColor: Palette.brand.voidBackground, | |
39 | + height: 1, | |
40 | + }, | |
41 | +}); | |
42 | + | |
43 | +class RawFeedItemSeparator extends PureComponent { | |
44 | + public render() { | |
45 | + return h(View, {style: styles.itemSeparator}); | |
46 | + } | |
47 | +} | |
48 | + | |
49 | +type Props = { | |
50 | + getReadable: GetReadable<MsgAndExtras> | null; | |
51 | + style?: any; | |
52 | +}; | |
53 | + | |
54 | +export default class Feed extends PureComponent<Props, {}> { | |
55 | + public render() { | |
56 | + const {style, getReadable} = this.props; | |
57 | + | |
58 | + return h<PullFlatListProps<MsgAndExtras>>(PullFlatList, { | |
59 | + getScrollStream: getReadable, | |
60 | + keyExtractor: (msg: MsgAndExtras, idx: number) => msg.key || String(idx), | |
61 | + style: [styles.container, style] as any, | |
62 | + initialNumToRender: 2, | |
63 | + pullAmount: 3, | |
64 | + numColumns: 1, | |
65 | + refreshable: true, | |
66 | + refreshColors: [Palette.indigo7], | |
67 | + ItemSeparatorComponent: RawFeedItemSeparator, | |
68 | + renderItem: ({item}) => | |
69 | + h(ShortRawMessageM, { | |
70 | + msg: item, | |
71 | + name: item.value._streams.about.name, | |
72 | + imageUrl: item.value._streams.about.imageUrl, | |
73 | + }), | |
74 | + }); | |
75 | + } | |
76 | +} |
src/app/drivers/ssb.ts | ||
---|---|---|
@@ -78,17 +78,20 @@ | ||
78 | 78 | } |
79 | 79 | |
80 | 80 | function mutateThreadWithLiveExtras(api: any) { |
81 | 81 | return (thread: ThreadData) => { |
82 | - thread.messages.forEach(msg => mutateMsgWithLiveExtras(api)(msg)); | |
82 | + for (const msg of thread.messages) { | |
83 | + mutateMsgWithLiveExtras(api)(msg); | |
84 | + } | |
83 | 85 | return thread; |
84 | 86 | }; |
85 | 87 | } |
86 | 88 | |
87 | 89 | export type GetReadable<T> = (opts?: any) => Readable<T>; |
88 | 90 | |
89 | 91 | export class SSBSource { |
90 | 92 | public selfFeedId$: Stream<FeedId>; |
93 | + public publicRawFeed$: Stream<GetReadable<MsgAndExtras>>; | |
91 | 94 | public publicFeed$: Stream<GetReadable<ThreadAndExtras>>; |
92 | 95 | public publicLiveUpdates$: Stream<null>; |
93 | 96 | public selfRoots$: Stream<GetReadable<ThreadAndExtras>>; |
94 | 97 | public selfReplies$: Stream<GetReadable<MsgAndExtras>>; |
@@ -97,8 +100,15 @@ | ||
97 | 100 | |
98 | 101 | constructor(private api$: Stream<any>) { |
99 | 102 | this.selfFeedId$ = api$.map(api => api.keys.sync.id[0]()); |
100 | 103 | |
104 | + this.publicRawFeed$ = api$.map(api => (opts?: any) => | |
105 | + pull( | |
106 | + api.sbot.pull.feed[0]({reverse: true, live: false, ...opts}), | |
107 | + pull.map(mutateMsgWithLiveExtras(api)), | |
108 | + ), | |
109 | + ); | |
110 | + | |
101 | 111 | this.publicFeed$ = api$.map(api => (opts?: any) => |
102 | 112 | pull( |
103 | 113 | api.sbot.pull.publicThreads[0]({reverse: true, live: false, ...opts}), |
104 | 114 | pull.map(mutateThreadWithLiveExtras(api)), |
src/app/index.ts | ||
---|---|---|
@@ -23,8 +23,9 @@ | ||
23 | 23 | Compose = 'mmmmm.Compose', |
24 | 24 | Thread = 'mmmmm.Thread', |
25 | 25 | Profile = 'mmmmm.Profile', |
26 | 26 | ProfileEdit = 'mmmmm.Profile.Edit', |
27 | + RawDatabase = 'mmmmm.RawDatabase', | |
27 | 28 | } |
28 | 29 | |
29 | 30 | import onionify from 'cycle-onionify'; |
30 | 31 | import {makeKeyboardDriver} from 'cycle-native-keyboard'; |
@@ -38,8 +39,9 @@ | ||
38 | 39 | import {compose} from './screens/compose/index'; |
39 | 40 | import {thread} from './screens/thread/index'; |
40 | 41 | import {profile} from './screens/profile/index'; |
41 | 42 | import {editProfile} from './screens/profile-edit/index'; |
43 | +import {rawDatabase} from './screens/raw-db/index'; | |
42 | 44 | import {Palette} from './global-styles/palette'; |
43 | 45 | import {Typography} from './global-styles/typography'; |
44 | 46 | import {addDisclaimer} from './alpha-disclaimer'; |
45 | 47 | |
@@ -49,8 +51,9 @@ | ||
49 | 51 | [Screens.Compose]: onionify(compose), |
50 | 52 | [Screens.Thread]: addDisclaimer(onionify(thread)), |
51 | 53 | [Screens.Profile]: addDisclaimer(onionify(profile)), |
52 | 54 | [Screens.ProfileEdit]: addDisclaimer(onionify(editProfile)), |
55 | + [Screens.RawDatabase]: addDisclaimer(rawDatabase), | |
53 | 56 | }; |
54 | 57 | |
55 | 58 | export const drivers = { |
56 | 59 | alert: alertDriver, |
src/app/screens/drawer/intent.ts | ||
---|---|---|
@@ -25,6 +25,8 @@ | ||
25 | 25 | |
26 | 26 | openAbout$: source.select('about').events('press').mapTo(null), |
27 | 27 | |
28 | 28 | emailBugReport$: source.select('bug-report').events('press').mapTo(null), |
29 | + | |
30 | + showRawDatabase$: source.select('raw-db').events('press').mapTo(null), | |
29 | 31 | }; |
30 | 32 | } |
src/app/screens/drawer/navigation.ts | ||
---|---|---|
@@ -18,36 +18,26 @@ | ||
18 | 18 | */ |
19 | 19 | |
20 | 20 | import xs, {Stream} from 'xstream'; |
21 | 21 | import sample from 'xstream-sample'; |
22 | -import {Command} from 'cycle-native-navigation'; | |
22 | +import {Command, PushCommand} from 'cycle-native-navigation'; | |
23 | 23 | import {navOptions as profileScreenNavOptions} from '../profile'; |
24 | +import {navOptions as rawDatabaseScreenNavOptions} from '../raw-db'; | |
24 | 25 | import {State} from './model'; |
25 | 26 | import {Screens} from '../..'; |
26 | 27 | |
27 | 28 | export type Actions = { |
28 | 29 | goToSelfProfile$: Stream<null>; |
30 | + showRawDatabase$: Stream<null>; | |
29 | 31 | }; |
30 | 32 | |
31 | 33 | export default function navigationCommands( |
32 | 34 | actions: Actions, |
33 | 35 | state$: Stream<State>, |
34 | 36 | ): Stream<Command> { |
35 | - const toSelfProfile$ = actions.goToSelfProfile$ | |
36 | - .compose(sample(state$)) | |
37 | - .map(state => { | |
38 | - const hideDrawer: Command = { | |
39 | - type: 'mergeOptions', | |
40 | - opts: { | |
41 | - sideMenu: { | |
42 | - left: { | |
43 | - visible: false, | |
44 | - }, | |
45 | - }, | |
46 | - }, | |
47 | - }; | |
48 | - | |
49 | - const openSelfProfile: Command = { | |
37 | + const toSelfProfile$ = actions.goToSelfProfile$.compose(sample(state$)).map( | |
38 | + state => | |
39 | + ({ | |
50 | 40 | type: 'push', |
51 | 41 | id: 'mainstack', |
52 | 42 | layout: { |
53 | 43 | component: { |
@@ -58,12 +48,41 @@ | ||
58 | 48 | }, |
59 | 49 | options: profileScreenNavOptions, |
60 | 50 | }, |
61 | 51 | }, |
52 | + } as PushCommand), | |
53 | + ); | |
54 | + | |
55 | + const toRawDatabase$ = actions.showRawDatabase$.map( | |
56 | + () => | |
57 | + ({ | |
58 | + type: 'push', | |
59 | + id: 'mainstack', | |
60 | + layout: { | |
61 | + component: { | |
62 | + name: Screens.RawDatabase, | |
63 | + options: rawDatabaseScreenNavOptions, | |
64 | + }, | |
65 | + }, | |
66 | + } as PushCommand), | |
67 | + ); | |
68 | + | |
69 | + const hideDrawerAndPush$ = xs | |
70 | + .merge(toSelfProfile$, toRawDatabase$) | |
71 | + .map(pushCommand => { | |
72 | + const hideDrawer: Command = { | |
73 | + type: 'mergeOptions', | |
74 | + opts: { | |
75 | + sideMenu: { | |
76 | + left: { | |
77 | + visible: false, | |
78 | + }, | |
79 | + }, | |
80 | + }, | |
62 | 81 | }; |
63 | 82 | |
64 | - return xs.of<Command>(hideDrawer, openSelfProfile); | |
83 | + return xs.of<Command>(hideDrawer, pushCommand); | |
65 | 84 | }) |
66 | 85 | .flatten(); |
67 | 86 | |
68 | - return toSelfProfile$; | |
87 | + return hideDrawerAndPush$; | |
69 | 88 | } |
src/app/screens/drawer/view.ts | ||
---|---|---|
@@ -64,10 +64,16 @@ | ||
64 | 64 | text: 'Email bug report', |
65 | 65 | accessible: true, |
66 | 66 | accessibilityLabel: 'Email bug report', |
67 | 67 | }), |
68 | - h(DrawerMenuItem, {icon: 'database', text: 'Raw database'}), | |
69 | 68 | h(DrawerMenuItem, { |
69 | + sel: 'raw-db', | |
70 | + icon: 'database', | |
71 | + text: 'Raw database', | |
72 | + accessible: true, | |
73 | + accessibilityLabel: 'Show Raw Database', | |
74 | + }), | |
75 | + h(DrawerMenuItem, { | |
70 | 76 | sel: 'about', |
71 | 77 | icon: 'information', |
72 | 78 | text: 'About MMMMM', |
73 | 79 | accessible: true, |
src/app/screens/raw-db/README.md | ||
---|---|---|
@@ -1,0 +1,1 @@ | ||
1 | +This Cycle.js component represents the screen where all SSB messages, regardless of type, are displayed in a scrolling view. |
src/app/screens/raw-db/index.ts | ||
---|---|---|
@@ -1,0 +1,66 @@ | ||
1 | +/** | |
2 | + * MMMMM is a mobile app for Secure Scuttlebutt networks | |
3 | + * | |
4 | + * Copyright (C) 2017 Andre 'Staltz' Medeiros | |
5 | + * | |
6 | + * This program is free software: you can redistribute it and/or modify | |
7 | + * it under the terms of the GNU General Public License as published by | |
8 | + * the Free Software Foundation, either version 3 of the License, or | |
9 | + * (at your option) any later version. | |
10 | + * | |
11 | + * This program is distributed in the hope that it will be useful, | |
12 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
13 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
14 | + * GNU General Public License for more details. | |
15 | + * | |
16 | + * You should have received a copy of the GNU General Public License | |
17 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. | |
18 | + */ | |
19 | + | |
20 | +import {Stream} from 'xstream'; | |
21 | +import {Command, PopCommand, NavSource} from 'cycle-native-navigation'; | |
22 | +import {SSBSource} from '../../drivers/ssb'; | |
23 | +import {ReactSource, h} from '@cycle/react'; | |
24 | +import {ReactElement} from 'react'; | |
25 | +import {Dimensions} from '../../global-styles/dimens'; | |
26 | +import RawFeed from '../../components/RawFeed'; | |
27 | + | |
28 | +export type Sources = { | |
29 | + screen: ReactSource; | |
30 | + navigation: NavSource; | |
31 | + ssb: SSBSource; | |
32 | +}; | |
33 | + | |
34 | +export type Sinks = { | |
35 | + screen: Stream<ReactElement<any>>; | |
36 | + navigation: Stream<Command>; | |
37 | +}; | |
38 | + | |
39 | +export const navOptions = { | |
40 | + topBar: { | |
41 | + height: Dimensions.toolbarAndroidHeight, | |
42 | + title: { | |
43 | + text: 'Raw database', | |
44 | + }, | |
45 | + backButton: { | |
46 | + icon: require('../../../../images/icon-arrow-left.png'), | |
47 | + visible: true, | |
48 | + }, | |
49 | + }, | |
50 | +}; | |
51 | + | |
52 | +export function rawDatabase(sources: Sources): Sinks { | |
53 | + const vdom$ = sources.ssb.publicRawFeed$.map(getReadable => | |
54 | + h(RawFeed, {getReadable}), | |
55 | + ); | |
56 | + const command$ = sources.navigation.backPress().mapTo( | |
57 | + { | |
58 | + type: 'pop', | |
59 | + } as PopCommand, | |
60 | + ); | |
61 | + | |
62 | + return { | |
63 | + screen: vdom$, | |
64 | + navigation: command$, | |
65 | + }; | |
66 | +} |
src/ssb/opinions/sbot.ts | ||
---|---|---|
@@ -228,15 +228,9 @@ | ||
228 | 228 | ...opts, |
229 | 229 | }); |
230 | 230 | }), |
231 | 231 | feed: rec.source((opts: any) => { |
232 | - return pull( | |
233 | - pullMore(sbot.createFeedStream, {...opts, limit: 10}, [ | |
234 | - 'value', | |
235 | - 'timestamp', | |
236 | - ]), | |
237 | - pull.through(runHooks), | |
238 | - ); | |
232 | + return sbot.createFeedStream(opts); | |
239 | 233 | }), |
240 | 234 | log: rec.source((opts: any) => { |
241 | 235 | return pull(sbot.createLogStream(opts), pull.through(runHooks)); |
242 | 236 | }), |
Built with git-ssb-web