git ssb

1+

Daan Patchwork / manyverse



Commit 152b3220024a49f51e890826e63050a72b729943

ux: fix #59, thread replies don't disappear

Andre Staltz committed on 6/18/2018, 1:42:38 PM
Parent: fc9f4578a3d9fa17c75d7e4fdba5b6d11a182df5

Files changed

src/app/components/FullThread.tschanged
src/app/screens/thread/index.tschanged
src/app/screens/thread/intent.tschanged
src/app/screens/thread/model.tschanged
src/app/screens/thread/view.tschanged
src/app/components/FullThread.tsView
@@ -17,35 +17,32 @@
1717 * along with this program. If not, see <http://www.gnu.org/licenses/>.
1818 */
1919
2020 import {Stream, Subscription, Listener} from 'xstream';
21-import {PureComponent, ReactElement} from 'react';
21+import {Component, ReactElement} from 'react';
2222 import {h} from '@cycle/native-screen';
2323 import {FeedId} from 'ssb-typescript';
24-import {ThreadAndExtras, MsgAndExtras, GetReadable} from '../drivers/ssb';
24+import {ThreadAndExtras, MsgAndExtras} from '../drivers/ssb';
2525 import Message from './messages/Message';
2626 import PlaceholderMessage from './messages/PlaceholderMessage';
27-const pull = require('pull-stream');
2827
2928 export type Props = {
3029 thread: ThreadAndExtras;
31- getPublicationsReadable?: GetReadable<MsgAndExtras> | null;
3230 publication$?: Stream<any> | null;
3331 selfFeedId: FeedId;
3432 onPressLike?: (ev: {msgKey: string; like: boolean}) => void;
3533 onPressAuthor?: (ev: {authorFeedId: FeedId}) => void;
3634 };
3735
3836 type State = {
3937 showPlaceholder: boolean;
40- thread: ThreadAndExtras;
4138 };
4239
43-export default class FullThread extends PureComponent<Props, State> {
40+export default class FullThread extends Component<Props, State> {
4441 constructor(props: Props) {
4542 super(props);
4643 this.renderMessage = this.renderMessage.bind(this);
47- this.state = {showPlaceholder: false, thread: this.props.thread};
44+ this.state = {showPlaceholder: false};
4845 }
4946
5047 private subscription?: Subscription;
5148
@@ -56,40 +53,38 @@
5653 this.subscription = publication$.subscribe(listener as Listener<any>);
5754 }
5855 }
5956
60- public componentWillReceiveProps(nextProps: Props) {
61- this.setState(prev => ({...prev, thread: nextProps.thread}));
57+ public shouldComponentUpdate(nextProps: Props, nextState: State) {
58+ const prevProps = this.props;
59+ if (nextProps.selfFeedId !== prevProps.selfFeedId) return true;
60+ if (nextProps.onPressAuthor !== prevProps.onPressAuthor) return true;
61+ if (nextProps.onPressLike !== prevProps.onPressLike) return true;
62+ if (nextProps.publication$ !== prevProps.publication$) return true;
63+ const prevMessages = prevProps.thread.messages;
64+ const nextMessages = nextProps.thread.messages;
65+ if (nextMessages.length !== prevMessages.length) return true;
66+ if (nextState.showPlaceholder !== this.state.showPlaceholder) return true;
67+ return false;
6268 }
6369
70+ public componentDidUpdate(prevProps: Props, prevState: State) {
71+ const prevMessages = prevProps.thread.messages;
72+ const nextMessages = this.props.thread.messages;
73+ if (nextMessages.length > prevMessages.length) {
74+ this.setState({showPlaceholder: false});
75+ }
76+ }
77+
6478 public componentWillUnmount() {
6579 if (this.subscription) {
6680 this.subscription.unsubscribe();
6781 this.subscription = void 0;
6882 }
6983 }
7084
7185 private onPublication() {
72- const {getPublicationsReadable} = this.props;
73- if (!getPublicationsReadable) return;
74- const readable = getPublicationsReadable({live: true, old: false});
75- if (!readable) return;
76- const that = this;
77-
78- this.setState(() => ({showPlaceholder: true}));
79- pull(
80- readable,
81- pull.take(1),
82- pull.drain((msg: MsgAndExtras) => {
83- that.setState((prev: State) => ({
84- showPlaceholder: false,
85- thread: {
86- messages: prev.thread.messages.concat([msg]),
87- full: prev.thread.full,
88- },
89- }));
90- }),
91- );
86+ this.setState({showPlaceholder: true});
9287 }
9388
9489 private renderMessage(msg: MsgAndExtras) {
9590 const {selfFeedId, onPressLike, onPressAuthor} = this.props;
@@ -102,9 +97,9 @@
10297 });
10398 }
10499
105100 public render() {
106- const {thread} = this.state;
101+ const thread = this.props.thread;
107102 if (!thread.messages || thread.messages.length <= 0) return [];
108103 const children: Array<ReactElement<any>> = thread.messages.map(
109104 this.renderMessage,
110105 );
src/app/screens/thread/index.tsView
@@ -51,12 +51,12 @@
5151 title: 'Thread',
5252 });
5353
5454 export function thread(sources: Sources): Sinks {
55- const actions = intent(sources.screen, sources.onion.state$);
55+ const actions = intent(sources.screen, sources.ssb, sources.onion.state$);
5656 const reducer$ = model(sources.onion.state$, actions, sources.ssb);
5757 const command$ = navigation(actions);
58- const vdom$ = view(sources.onion.state$, sources.ssb, actions);
58+ const vdom$ = view(sources.onion.state$, actions);
5959 const newContent$ = ssb(actions);
6060 const dismiss$ = actions.publishMsg$.mapTo('dismiss' as 'dismiss');
6161
6262 return {
src/app/screens/thread/intent.tsView
@@ -19,24 +19,32 @@
1919
2020 import {Stream} from 'xstream';
2121 import sample from 'xstream-sample';
2222 import {ScreensSource} from 'cycle-native-navigation';
23+import {isReplyPostMsg} from 'ssb-typescript/utils';
2324 import {FeedId} from 'ssb-typescript';
2425 import {Screens} from '../..';
2526 import {State} from './model';
27+import {SSBSource} from '../../drivers/ssb';
2628
2729 export type ProfileNavEvent = {authorFeedId: FeedId};
2830
2931 export type LikeEvent = {msgKey: string; like: boolean};
3032
31-export default function intent(source: ScreensSource, state$: Stream<State>) {
33+export default function intent(
34+ source: ScreensSource,
35+ ssbSource: SSBSource,
36+ state$: Stream<State>,
37+) {
3238 return {
3339 publishMsg$: source
3440 .select('replyButton')
3541 .events('press')
3642 .compose(sample(state$))
3743 .filter(state => !!state.replyText && !!state.rootMsgId),
3844
45+ willReply$: ssbSource.publishHook$.filter(isReplyPostMsg),
46+
3947 appear$: source.willAppear(Screens.Thread).mapTo(null),
4048
4149 disappear$: source.didDisappear(Screens.Thread).mapTo(null),
4250
src/app/screens/thread/model.tsView
@@ -19,8 +19,9 @@
1919
2020 import xs, {Stream} from 'xstream';
2121 import sample from 'xstream-sample';
2222 import dropRepeats from 'xstream/extra/dropRepeats';
23+import xsFromPullStream from 'xstream-from-pull-stream';
2324 import {Reducer} from 'cycle-onionify';
2425 import {FeedId, MsgId} from 'ssb-typescript';
2526 import {
2627 ThreadAndExtras,
@@ -69,8 +70,9 @@
6970 }
7071
7172 export type AppearingActions = {
7273 publishMsg$: Stream<any>;
74+ willReply$: Stream<any>;
7375 appear$: Stream<null>;
7476 disappear$: Stream<null>;
7577 updateReplyText$: Stream<string>;
7678 };
@@ -122,17 +124,34 @@
122124 ),
123125 )
124126 .flatten();
125127
126- const updateSelfRepliesReducer$ = ssbSource.selfReplies$.map(
127- getReadable =>
128- function updateSelfRepliesReducer(prev?: State): State {
129- if (!prev) {
130- throw new Error('Thread/model reducer expects existing state');
131- }
132- return {...prev, getSelfRepliesReadable: getReadable};
133- },
134- );
128+ const addSelfRepliesReducer$ = actions.willReply$
129+ .map(() =>
130+ ssbSource.selfReplies$
131+ .map(getReadable =>
132+ xsFromPullStream<MsgAndExtras>(
133+ getReadable({live: true, old: false}),
134+ ).take(1),
135+ )
136+ .flatten(),
137+ )
138+ .flatten()
139+ .map(
140+ newMsg =>
141+ function addSelfRepliesReducer(prev?: State): State {
142+ if (!prev) {
143+ throw new Error('Thread/model reducer expects existing state');
144+ }
145+ return {
146+ ...prev,
147+ thread: {
148+ messages: prev.thread.messages.concat([newMsg]),
149+ full: true,
150+ },
151+ };
152+ },
153+ );
135154
136155 const clearReplyReducer$ = actions.disappear$.mapTo(
137156 function clearReplyReducer(prev?: State): State {
138157 if (!prev) {
@@ -145,8 +164,8 @@
145164 return xs.merge(
146165 setThreadReducer$,
147166 updateReplyTextReducer$,
148167 publishReplyReducers$,
149- updateSelfRepliesReducer$,
168+ addSelfRepliesReducer$,
150169 clearReplyReducer$,
151170 );
152171 }
src/app/screens/thread/view.tsView
@@ -17,14 +17,14 @@
1717 * along with this program. If not, see <http://www.gnu.org/licenses/>.
1818 */
1919
2020 import {Stream} from 'xstream';
21+import dropRepeats from 'xstream/extra/dropRepeats';
2122 import {h} from '@cycle/native-screen';
2223 import * as Progress from 'react-native-progress';
2324 import {View, TextInput, ScrollView, TouchableOpacity} from 'react-native';
2425 import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
2526 import {propifyMethods} from 'react-propify-methods';
26-import {isReplyPostMsg} from 'ssb-typescript/utils';
2727 import {Screens} from '../..';
2828 import {Palette} from '../../global-styles/palette';
2929 import {Dimensions} from '../../global-styles/dimens';
3030 import {SSBSource} from '../../drivers/ssb';
@@ -87,37 +87,39 @@
8787 const ReactiveScrollView = propifyMethods(ScrollView, 'scrollToEnd' as any);
8888
8989 type Actions = {
9090 publishMsg$: Stream<any>;
91+ willReply$: Stream<any>;
9192 };
9293
93-export default function view(
94- state$: Stream<State>,
95- ssbSource: SSBSource,
96- actions: Actions,
97-) {
98- return state$.map((state: State) => {
94+function statesAreEqual(s1: State, s2: State): boolean {
95+ if (s1.replyText !== s2.replyText) return false;
96+ if (s1.replyEditable !== s2.replyEditable) return false;
97+ if (s1.startedAsReply !== s2.startedAsReply) return false;
98+ if (s1.thread.messages.length !== s2.thread.messages.length) return false;
99+ if (s1.thread.full !== s2.thread.full) return false;
100+ if (s1.getSelfRepliesReadable !== s2.getSelfRepliesReadable) return false;
101+ if (s1.rootMsgId !== s2.rootMsgId) return false;
102+ if (s1.selfFeedId !== s2.selfFeedId) return false;
103+ return true;
104+}
105+
106+export default function view(state$: Stream<State>, actions: Actions) {
107+ const scrollToEnd$ = actions.publishMsg$.mapTo({animated: false});
108+ return state$.compose(dropRepeats(statesAreEqual)).map((state: State) => {
99109 return {
100110 screen: Screens.Thread,
101111 vdom: h(View, {style: styles.container}, [
102- h(
103- ReactiveScrollView,
104- {
105- style: styles.scrollView,
106- scrollToEnd$: actions.publishMsg$.mapTo({animated: false}),
107- },
108- [
109- state.thread.messages.length === 0
110- ? Loading
111- : h(FullThread, {
112- selector: 'thread',
113- thread: state.thread,
114- selfFeedId: state.selfFeedId,
115- publication$: ssbSource.publishHook$.filter(isReplyPostMsg),
116- getPublicationsReadable: state.getSelfRepliesReadable,
117- }),
118- ],
119- ),
112+ h(ReactiveScrollView, {style: styles.scrollView, scrollToEnd$}, [
113+ state.thread.messages.length === 0
114+ ? Loading
115+ : h(FullThread, {
116+ selector: 'thread',
117+ thread: state.thread,
118+ selfFeedId: state.selfFeedId,
119+ publication$: actions.willReply$,
120+ }),
121+ ]),
120122 ReplyInput(state),
121123 ]),
122124 };
123125 });

Built with git-ssb-web