Verified Commit e402d4c6 authored by Jacob Karlsson's avatar Jacob Karlsson Committed by staltz

ux: Feature: see which accounts liked a message

parent f06b65c3
......@@ -9,3 +9,4 @@ Gordon Martin (happy0) <gordon.hugh.martin@gmail.com>
Luandro <luandro@gmail.com>
Robert P. Levy (rplevy) <r.p.levy@gmail.com>
Charles E. Lehner (cel) <cel@celehner.com>
Jacob Karlsson (Powersource) <jacob.karlsson95@gmail.com>
......@@ -9340,8 +9340,7 @@
},
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"optional": true
"bundled": true
},
"aproba": {
"version": "1.2.0",
......@@ -9359,13 +9358,11 @@
},
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"optional": true
"bundled": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
......@@ -9378,18 +9375,15 @@
},
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"optional": true
"bundled": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"optional": true
"bundled": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"optional": true
"bundled": true
},
"core-util-is": {
"version": "1.0.2",
......@@ -9492,8 +9486,7 @@
},
"inherits": {
"version": "2.0.3",
"bundled": true,
"optional": true
"bundled": true
},
"ini": {
"version": "1.3.5",
......@@ -9503,7 +9496,6 @@
"is-fullwidth-code-point": {
"version": "1.0.0",
"bundled": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
......@@ -9516,20 +9508,17 @@
"minimatch": {
"version": "3.0.4",
"bundled": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "0.0.8",
"bundled": true,
"optional": true
"bundled": true
},
"minipass": {
"version": "2.3.5",
"bundled": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
......@@ -9546,7 +9535,6 @@
"mkdirp": {
"version": "0.5.1",
"bundled": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
......@@ -9619,8 +9607,7 @@
},
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"optional": true
"bundled": true
},
"object-assign": {
"version": "4.1.1",
......@@ -9630,7 +9617,6 @@
"once": {
"version": "1.4.0",
"bundled": true,
"optional": true,
"requires": {
"wrappy": "1"
}
......@@ -9706,8 +9692,7 @@
},
"safe-buffer": {
"version": "5.1.2",
"bundled": true,
"optional": true
"bundled": true
},
"safer-buffer": {
"version": "2.1.2",
......@@ -9737,7 +9722,6 @@
"string-width": {
"version": "1.0.2",
"bundled": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
......@@ -9755,7 +9739,6 @@
"strip-ansi": {
"version": "3.0.1",
"bundled": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
......@@ -9794,13 +9777,11 @@
},
"wrappy": {
"version": "1.0.2",
"bundled": true,
"optional": true
"bundled": true
},
"yallist": {
"version": "3.0.3",
"bundled": true,
"optional": true
"bundled": true
}
}
},
......@@ -17775,4 +17756,4 @@
"integrity": "sha512-YO803/X+13GNaZB7fVopjvHH0uWQKgJkgKnU1YCjxShjKGVuN9PPHHW8g+uFDpkHpSTNi3rCMKMewIcbC1BAYg=="
}
}
}
}
\ No newline at end of file
/* Copyright (C) 2018-2019 The Manyverse Authors.
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import {PureComponent} from 'react';
import {h} from '@cycle/react';
import {Text, View, TouchableNativeFeedback, StyleSheet} from 'react-native';
import {FeedId} from 'ssb-typescript';
import {Dimensions} from '../global-styles/dimens';
import {Palette} from '../global-styles/palette';
import {Typography} from '../global-styles/typography';
import Avatar from './Avatar';
import React = require('react');
export const styles = StyleSheet.create({
row: {
flex: 1,
backgroundColor: Palette.backgroundText,
paddingHorizontal: Dimensions.horizontalSpaceBig,
paddingVertical: Dimensions.verticalSpaceBig,
marginBottom: 1,
flexDirection: 'row',
},
avatar: {
marginRight: Dimensions.horizontalSpaceSmall,
},
authorColumn: {
flexDirection: 'column',
flex: 1,
alignItems: 'flex-start',
justifyContent: 'space-around',
},
authorName: {
fontSize: Typography.fontSizeNormal,
fontWeight: 'bold',
fontFamily: Typography.fontFamilyReadableText,
color: Palette.text,
minWidth: 120,
},
msgType: {
fontSize: Typography.fontSizeSmall,
fontFamily: Typography.fontFamilyMonospace,
backgroundColor: Palette.backgroundTextHacker,
color: Palette.textHacker,
},
timestamp: {
fontSize: Typography.fontSizeSmall,
fontFamily: Typography.fontFamilyReadableText,
color: Palette.textWeak,
},
});
type AccountProps = {
name: string;
imageUrl?: string;
id: string;
onPress?: () => void;
};
class Account extends PureComponent<AccountProps> {
public render() {
const {name, imageUrl, onPress} = this.props;
const touchableProps = {
background: TouchableNativeFeedback.SelectableBackground(),
onPress,
};
const authorNameText = h(
Text,
{
numberOfLines: 1,
ellipsizeMode: 'middle',
style: styles.authorName,
},
name,
);
return h(View, [
h(TouchableNativeFeedback, touchableProps, [
h(View, {style: styles.row}, [
h(Avatar, {
url: imageUrl,
size: Dimensions.avatarSizeNormal,
style: styles.avatar,
}),
h(View, {style: styles.authorColumn}, [authorNameText]),
]),
]),
]);
}
}
export type Props = {
accounts: Array<{name: string; imageUrl: string; id: string}>;
onPressAccount?: (ev: {id: FeedId}) => void;
};
export default class AccountsList extends PureComponent<Props> {
public render() {
const {onPressAccount} = this.props;
return h(
React.Fragment,
this.props.accounts.map(({id, name, imageUrl}) =>
h<AccountProps>(Account, {
name,
imageUrl,
id,
onPress: () => onPressAccount && onPressAccount({id}),
}),
),
);
}
}
......@@ -7,13 +7,14 @@
import {PureComponent} from 'react';
import {h} from '@cycle/react';
import {FeedId, MsgId, Msg} from 'ssb-typescript';
import {ThreadAndExtras, MsgAndExtras} from '../drivers/ssb';
import {ThreadAndExtras, MsgAndExtras, Likes} from '../drivers/ssb';
import Message from './messages/Message';
import ExpandThread from './messages/ExpandThread';
export type Props = {
thread: ThreadAndExtras;
selfFeedId: FeedId;
onPressLikeCount?: (ev: {msgKey: MsgId; likes: Likes}) => void;
onPressLike?: (ev: {msgKey: MsgId; like: boolean}) => void;
onPressReply?: (ev: {msgKey: MsgId; rootKey: MsgId}) => void;
onPressAuthor?: (ev: {authorFeedId: FeedId}) => void;
......@@ -29,6 +30,7 @@ export default class CompactThread extends PureComponent<Props> {
private renderMessage(msg: MsgAndExtras) {
const {
selfFeedId,
onPressLikeCount,
onPressLike,
onPressReply,
onPressAuthor,
......@@ -39,6 +41,7 @@ export default class CompactThread extends PureComponent<Props> {
msg,
['key' as any]: msg.key,
selfFeedId,
onPressLikeCount,
onPressLike,
onPressReply,
onPressAuthor,
......
......@@ -19,7 +19,7 @@ import {Dimensions} from '../global-styles/dimens';
import {Palette} from '../global-styles/palette';
import CompactThread from './CompactThread';
import PlaceholderMessage from './messages/PlaceholderMessage';
import {GetReadable, ThreadAndExtras} from '../drivers/ssb';
import {GetReadable, ThreadAndExtras, Likes} from '../drivers/ssb';
import PullFlatList from 'pull-flat-list';
import {Stream, Subscription, Listener} from 'xstream';
import {propifyMethods} from 'react-propify-methods';
......@@ -127,6 +127,7 @@ type Props = {
style?: any;
onInitialPullDone?: () => void;
onRefresh?: () => void;
onPressLikeCount?: (ev: {msgKey: MsgId; likes: Likes}) => void;
onPressLike?: (ev: {msgKey: MsgId; like: boolean}) => void;
onPressReply?: (ev: {msgKey: MsgId; rootKey: MsgId}) => void;
onPressAuthor?: (ev: {authorFeedId: FeedId}) => void;
......@@ -207,6 +208,7 @@ export default class Feed extends PureComponent<Props, State> {
public render() {
const {
onRefresh,
onPressLikeCount,
onPressLike,
onPressReply,
onPressAuthor,
......@@ -248,6 +250,7 @@ export default class Feed extends PureComponent<Props, State> {
h(CompactThread, {
thread: item as ThreadAndExtras,
selfFeedId,
onPressLikeCount,
onPressLike,
onPressReply,
onPressAuthor,
......
......@@ -7,8 +7,8 @@
import {Stream, Subscription, Listener} from 'xstream';
import {Component, ReactElement} from 'react';
import {h} from '@cycle/react';
import {FeedId, Msg} from 'ssb-typescript';
import {ThreadAndExtras, MsgAndExtras} from '../drivers/ssb';
import {FeedId, Msg, MsgId} from 'ssb-typescript';
import {ThreadAndExtras, MsgAndExtras, Likes} from '../drivers/ssb';
import Message from './messages/Message';
import PlaceholderMessage from './messages/PlaceholderMessage';
......@@ -16,6 +16,7 @@ export type Props = {
thread: ThreadAndExtras;
publication$?: Stream<any> | null;
selfFeedId: FeedId;
onPressLikeCount?: (ev: {msgKey: MsgId; likes: Likes}) => void;
onPressLike?: (ev: {msgKey: string; like: boolean}) => void;
onPressAuthor?: (ev: {authorFeedId: FeedId}) => void;
onPressEtc?: (msg: Msg) => void;
......@@ -47,6 +48,7 @@ export default class FullThread extends Component<Props, State> {
if (nextProps.selfFeedId !== prevProps.selfFeedId) return true;
if (nextProps.onPressAuthor !== prevProps.onPressAuthor) return true;
if (nextProps.onPressEtc !== prevProps.onPressEtc) return true;
if (nextProps.onPressLikeCount !== prevProps.onPressLikeCount) return true;
if (nextProps.onPressLike !== prevProps.onPressLike) return true;
if (nextProps.publication$ !== prevProps.publication$) return true;
const prevMessages = prevProps.thread.messages;
......@@ -76,11 +78,18 @@ export default class FullThread extends Component<Props, State> {
}
private renderMessage(msg: MsgAndExtras) {
const {selfFeedId, onPressLike, onPressAuthor, onPressEtc} = this.props;
const {
selfFeedId,
onPressLikeCount,
onPressLike,
onPressAuthor,
onPressEtc,
} = this.props;
return h(Message, {
msg,
['key' as any]: msg.key,
selfFeedId,
onPressLikeCount,
onPressLike,
onPressAuthor,
onPressEtc,
......
......@@ -9,7 +9,7 @@ import {PureComponent} from 'react';
import {h} from '@cycle/react';
import {Msg, FeedId, MsgId} from 'ssb-typescript';
import {isPostMsg, isContactMsg, isAboutMsg} from 'ssb-typescript/utils';
import {MsgAndExtras} from '../../drivers/ssb';
import {MsgAndExtras, Likes} from '../../drivers/ssb';
import RawMessage from './RawMessage';
import PostMessage from './PostMessage';
import AboutMessage from './AboutMessage';
......@@ -24,6 +24,7 @@ export type State = {
export type Props = {
msg: MsgAndExtras;
selfFeedId: FeedId;
onPressLikeCount?: (ev: {msgKey: MsgId; likes: Likes}) => void;
onPressLike?: (ev: {msgKey: MsgId; like: boolean}) => void;
onPressReply?: (ev: {msgKey: MsgId; rootKey: MsgId}) => void;
onPressAuthor?: (ev: {authorFeedId: FeedId}) => void;
......
......@@ -18,6 +18,8 @@ import {Msg, FeedId, PostContent, MsgId} from 'ssb-typescript';
import {Palette} from '../../global-styles/palette';
import {Dimensions} from '../../global-styles/dimens';
import {Typography} from '../../global-styles/typography';
import {Likes} from '../../drivers/ssb';
import React = require('react');
export const styles = StyleSheet.create({
row: {
......@@ -30,18 +32,23 @@ export const styles = StyleSheet.create({
},
likeCount: {
flexDirection: 'row',
fontWeight: 'bold',
},
likes: {
marginTop: Dimensions.verticalSpaceSmall,
paddingTop: Dimensions.verticalSpaceSmall,
paddingBottom: Dimensions.verticalSpaceSmall,
paddingRight: Dimensions.horizontalSpaceSmall,
fontSize: Typography.fontSizeSmall,
fontFamily: Typography.fontFamilyReadableText,
color: Palette.textWeak,
},
likesHidden: {
marginTop: Dimensions.verticalSpaceSmall,
paddingTop: Dimensions.verticalSpaceSmall,
paddingBottom: Dimensions.verticalSpaceSmall,
paddingRight: Dimensions.horizontalSpaceSmall,
fontSize: Typography.fontSizeSmall,
fontFamily: Typography.fontFamilyReadableText,
color: Palette.backgroundText,
......@@ -49,7 +56,7 @@ export const styles = StyleSheet.create({
likeButton: {
flexDirection: 'row',
paddingTop: Dimensions.verticalSpaceSmall + 6,
paddingTop: Dimensions.verticalSpaceBig,
paddingBottom: Dimensions.verticalSpaceBig,
paddingLeft: 1,
paddingRight: Dimensions.horizontalSpaceBig,
......@@ -66,7 +73,7 @@ export const styles = StyleSheet.create({
replyButton: {
flexDirection: 'row',
paddingTop: Dimensions.verticalSpaceSmall + 6,
paddingTop: Dimensions.verticalSpaceBig,
paddingBottom: Dimensions.verticalSpaceBig,
paddingLeft: Dimensions.horizontalSpaceBig,
paddingRight: Dimensions.horizontalSpaceBig,
......@@ -108,7 +115,8 @@ const iconProps = {
export type Props = {
msg: Msg;
selfFeedId: FeedId;
likes: Array<FeedId> | null;
likes: Likes;
onPressLikeCount?: (ev: {msgKey: MsgId; likes: Likes}) => void;
onPressLike?: (ev: {msgKey: MsgId; like: boolean}) => void;
onPressReply?: (ev: {msgKey: MsgId; rootKey: MsgId}) => void;
};
......@@ -122,6 +130,12 @@ export default class MessageFooter extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = this.stateFromProps(props, {ilike: 'no', likeCount: 0});
this._likeCountButtonProps = {
background: TouchableNativeFeedback.SelectableBackground(),
onPress: this.onPressLikeCountHandler.bind(this),
accessible: true,
accessibilityLabel: 'Like count Button',
};
this._likeButtonProps = {
background: TouchableNativeFeedback.SelectableBackground(),
onPress: this.onPressLikeHandler.bind(this),
......@@ -136,6 +150,7 @@ export default class MessageFooter extends Component<Props, State> {
};
}
private _likeCountButtonProps: TouchableNativeFeedbackProperties;
private _likeButtonProps: TouchableNativeFeedbackProperties;
private _replyButtonProps: TouchableNativeFeedbackProperties;
......@@ -157,6 +172,15 @@ export default class MessageFooter extends Component<Props, State> {
}
}
private onPressLikeCountHandler() {
const msgKey = this.props.msg.key;
const likes = this.props.likes;
const onPressLikeCount = this.props.onPressLikeCount;
if (onPressLikeCount) {
onPressLikeCount({msgKey, likes});
}
}
private onPressLikeHandler() {
const ilike = this.state.ilike;
this.setState((prev: State) => ({
......@@ -198,20 +222,26 @@ export default class MessageFooter extends Component<Props, State> {
public render() {
const {likeCount, ilike} = this.state;
const counter = h(View, {style: styles.row}, [
h(
Text,
{
style: likeCount ? styles.likes : styles.likesHidden,
accessible: true,
accessibilityLabel: 'Like Count',
},
[
h(Text, {style: styles.likeCount}, String(likeCount)),
(likeCount === 1 ? ' like' : ' likes') as any,
],
),
]);
const likesComponent = [
h(View, {style: styles.col}, [
h(
Text,
{
style: likeCount ? styles.likes : styles.likesHidden,
accessible: true,
accessibilityLabel: 'Like Count',
},
[
h(Text, {style: styles.likeCount}, String(likeCount)),
(likeCount === 1 ? ' like' : ' likes') as any,
],
),
]),
];
const counter = likeCount
? h(TouchableNativeFeedback, this._likeCountButtonProps, likesComponent)
: h(React.Fragment, likesComponent);
const buttons = [
h(TouchableNativeFeedback, this._likeButtonProps, [
......@@ -234,7 +264,7 @@ export default class MessageFooter extends Component<Props, State> {
}
return h(View, {style: styles.col}, [
counter,
h(View, {style: styles.row}, [counter]),
h(View, {style: styles.row}, buttons),
]);
}
......
......@@ -12,6 +12,7 @@ import MessageHeader from './MessageHeader';
import MessageFooter from './MessageFooter';
import ContentWarning from './ContentWarning';
import {PostContent as Post, FeedId, Msg, MsgId} from 'ssb-typescript';
import {Likes} from '../../drivers/ssb';
type CWPost = Post & {contentWarning?: string};
......@@ -19,8 +20,9 @@ export type Props = {
msg: Msg<Post>;
name: string | null;
imageUrl: string | null;
likes: Array<FeedId> | null;
likes: Likes;
selfFeedId: FeedId;
onPressLikeCount?: (ev: {msgKey: MsgId; likes: Likes}) => void;
onPressLike?: (ev: {msgKey: MsgId; like: boolean}) => void;
onPressReply?: (ev: {msgKey: MsgId; rootKey: MsgId}) => void;
onPressAuthor?: (ev: {authorFeedId: FeedId}) => void;
......
......@@ -25,6 +25,8 @@ const ssbKeys = require('react-native-ssb-client-keys');
const depjectCombine = require('depject');
const colorHash = new (require('color-hash'))();
export type Likes = Array<FeedId> | null;
export type MsgAndExtras<C = Content> = Msg<C> & {
value: {
_$manyverse$metadata: {
......@@ -393,6 +395,34 @@ export class SSBSource {
);
}
public liteAbout$(ids: Array<FeedId>): Stream<Array<AboutAndExtras>> {
return this.api$
.map(async api => {
const aboutSocialValue = api.sbot.async.aboutSocialValue[0];
const abouts: Array<AboutAndExtras> = [];
for (const id of ids) {
// Fetch name
const [, result1] = await runAsync<string>(aboutSocialValue)({
key: 'name',
dest: id,
});
const name = result1 || shortFeedId(id);
// Fetch avatar
const [, result2] = await runAsync(aboutSocialValue)({
key: 'image',
dest: id,
});
const imageUrl = imageToImageUrl(result2);
abouts.push({name, imageUrl, id});
}
return abouts;
})
.map(promise => xs.fromPromise(promise))
.flatten();
}
public profileAbout$(id: FeedId): Stream<AboutAndExtras> {
return this.api$
.map(api => {
......
......@@ -14,6 +14,7 @@ export enum Screens {
Profile = 'Manyverse.Profile',
ProfileEdit = 'Manyverse.Profile.Edit',
Biography = 'Manyverse.Biography',
Accounts = 'Manyverse.Accounts',
RawDatabase = 'Manyverse.RawDatabase',
RawMessage = 'Manyverse.RawMessage',
}
......@@ -42,6 +43,7 @@ import {profile} from './screens/profile/index';
import {editProfile} from './screens/profile-edit/index';
import {createInvite} from './screens/invite-create';
import {biography} from './screens/biography/index';
import {accounts} from './screens/accounts/index';
import {rawDatabase} from './screens/raw-db/index';
import {rawMessage} from './screens/raw-msg/index';
import {Palette} from './global-styles/palette';
......@@ -57,6 +59,7 @@ export const screens: {[k in Screens]?: (so: any) => any} = {
[Screens.Profile]: withState(profile),
[Screens.ProfileEdit]: withState(editProfile),
[Screens.Biography]: withState(biography),
[Screens.Accounts]: withState(accounts),
[Screens.RawDatabase]: rawDatabase,
[Screens.RawMessage]: rawMessage,
};
......
This Cycle.js component represents screens where you can see a list of profiles, e.g. lists of who has liked a post, or who someone is following.
/* Copyright (C) 2018-2019 The Manyverse Authors.
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import xs, {Stream} from 'xstream';
import sampleCombine from 'xstream/extra/sampleCombine';
import {Command, PopCommand, NavSource} from 'cycle-native-navigation';
import {MsgId, About, FeedId} from 'ssb-typescript';
import {SSBSource, Likes} from '../../drivers/ssb';
import {ReactSource, h} from '@cycle/react';
import {ReactElement} from 'react';
import {Dimensions} from '../../global-styles/dimens';
import {navOptions as profileScreenNavOptions} from '../profile';
import {Screens} from '../..';
import {StyleSheet, ScrollView, RefreshControl} from 'react-native';
import {Reducer, StateSource} from '@cycle/state';
import AccountsList, {Props as ListProps} from '../../components/AccountsList';
import {Palette} from '../../global-styles/palette';
export type Props = {
selfFeedId: FeedId;
msgKey: MsgId;
likes: Likes;
};
export type Sources = {
props: Stream<Props>;
screen: ReactSource;
navigation: NavSource;
state: StateSource<State>;
ssb: SSBSource;
};
export type Sinks = {
screen: Stream<ReactElement<any>>;
navigation: Stream<Command>;
state: Stream<Reducer<State>>;
};
export type State = {
likers: Array<About>;
selfFeedId: FeedId;
};
export const styles = StyleSheet.create({
container: {
alignSelf: 'stretch',
flex: 1,
},
});
export const navOptions = {
topBar: {
visible: true,
drawBehind: false,
height: Dimensions.toolbarAndroidHeight,
title: {
text: 'Likes',
},
backButton: {