diff --git a/athena/utils/push-notifications/notification-formatting.js b/athena/utils/push-notifications/notification-formatting.js index 254fb78bd7..60dfbe1b38 100644 --- a/athena/utils/push-notifications/notification-formatting.js +++ b/athena/utils/push-notifications/notification-formatting.js @@ -1,6 +1,6 @@ import onlyContainsEmoji from 'shared/only-contains-emoji'; import sentencify from 'shared/sentencify'; -import { short as timeDifferenceShort } from 'shared/time-difference'; +import { timeDifferenceShort } from 'shared/time-difference'; import sortByDate from 'shared/sort-by-date'; import { toState, toPlainText } from 'shared/draft-utils'; diff --git a/mobile/components/Avatar/index.js b/mobile/components/Avatar/index.js index 5f4b28e836..98c4362a8a 100644 --- a/mobile/components/Avatar/index.js +++ b/mobile/components/Avatar/index.js @@ -9,15 +9,14 @@ import { AvatarImage } from './style'; type AvatarProps = {| src: string, - size: number, - radius: number, + size?: number, onPress?: Function, style?: Object, |}; class Avatar extends Component { render() { - const { src, size, radius, onPress, style } = this.props; + const { src, size, onPress, style } = this.props; let source = src ? { uri: src } : {}; return ( @@ -27,7 +26,7 @@ class Avatar extends Component { {children} )} > - + ); } diff --git a/mobile/components/Avatar/style.js b/mobile/components/Avatar/style.js index 762e8ce915..3f72025649 100644 --- a/mobile/components/Avatar/style.js +++ b/mobile/components/Avatar/style.js @@ -4,5 +4,5 @@ export const AvatarImage = styled.Image` background-color: ${props => props.theme.bg.border}; width: ${props => (props.size ? `${props.size}px` : '30px')}; height: ${props => (props.size ? `${props.size}px` : '30px')}; - border-radius: ${props => (props.radius ? `${props.radius}px` : '15px')}; + border-radius: ${props => (props.size ? `${props.size / 2}px` : '15px')}; `; diff --git a/mobile/components/Flex/Column.js b/mobile/components/Flex/Column.js new file mode 100644 index 0000000000..4dce9bb514 --- /dev/null +++ b/mobile/components/Flex/Column.js @@ -0,0 +1,6 @@ +// @flow +import styled from 'styled-components/native'; + +export default styled.View` + flex-direction: column; +`; diff --git a/mobile/components/Flex/Row.js b/mobile/components/Flex/Row.js new file mode 100644 index 0000000000..205aec1de3 --- /dev/null +++ b/mobile/components/Flex/Row.js @@ -0,0 +1,6 @@ +// @flow +import styled from 'styled-components/native'; + +export default styled.View` + flex-direction: row; +`; diff --git a/mobile/components/Message/index.js b/mobile/components/Message/index.js index c38e143daa..a3cd86c210 100644 --- a/mobile/components/Message/index.js +++ b/mobile/components/Message/index.js @@ -33,7 +33,12 @@ const Message = ({ message, me }: Props) => { case 'draftjs': { return ( - {body} + + {body} + ); } diff --git a/mobile/components/Messages/index.js b/mobile/components/Messages/index.js index 283e825124..9c832d14d2 100644 --- a/mobile/components/Messages/index.js +++ b/mobile/components/Messages/index.js @@ -1,6 +1,6 @@ // @flow import React, { Fragment } from 'react'; -import { View } from 'react-native'; +import { View, ScrollView } from 'react-native'; import { Query } from 'react-apollo'; import { withNavigation } from 'react-navigation'; import compose from 'recompose/compose'; @@ -8,6 +8,7 @@ import { getCurrentUserQuery } from '../../../shared/graphql/queries/user/getUse import viewNetworkHandler from '../ViewNetworkHandler'; import Text from '../Text'; import Message from '../Message'; +import InfiniteList from '../InfiniteList'; import { ThreadMargin } from '../../views/Thread/style'; import { sortAndGroupMessages } from '../../../shared/clients/group-messages'; import { convertTimestampToDate } from '../../../src/helpers/utils'; @@ -15,11 +16,13 @@ import { convertTimestampToDate } from '../../../src/helpers/utils'; import RoboText from './RoboText'; import Author from './Author'; +import type { FlatListProps } from 'react-native'; import type { Navigation } from 'react-navigation'; import type { ThreadMessageConnectionType } from '../../../shared/graphql/fragments/thread/threadMessageConnection.js'; import type { ThreadParticipantType } from '../../../shared/graphql/fragments/thread/threadParticipant'; type Props = { + ...$Exact, isLoading: boolean, hasError: boolean, navigation: Navigation, @@ -30,7 +33,13 @@ type Props = { class Messages extends React.Component { render() { - const { data, isLoading, hasError, navigation } = this.props; + const { + data, + isLoading, + hasError, + navigation, + ...flatListProps + } = this.props; if (data.messageConnection && data.messageConnection) { const messages = sortAndGroupMessages( @@ -45,8 +54,11 @@ class Messages extends React.Component { return ( {({ data: { user: currentUser } }) => ( - - {messages.map((group, i) => { + item[0].id} + renderItem={({ item: group, index: i }) => { if (group.length === 0) return null; const initialMessage = group[0]; @@ -122,8 +134,8 @@ class Messages extends React.Component { ); - })} - + }} + /> )} ); diff --git a/mobile/components/Text/index.js b/mobile/components/Text/index.js index c23a6a33bb..f350784dc8 100644 --- a/mobile/components/Text/index.js +++ b/mobile/components/Text/index.js @@ -32,9 +32,10 @@ const monospaceFont = Platform.OS === 'android' ? 'monospace' : 'Menlo'; const Text: ComponentType = styled.Text` ${(props: Props) => props.type && human[`${props.type}Object`]} - ${(props: Props) => - props.type && - `margin-top: ${human[`${props.type}Object`].lineHeight * 0.35};`} + ${(props: Props) => { + const type = props.type || 'body'; + return `margin-top: ${human[`${type}Object`].lineHeight * 0.35};`; + }} ${(props: Props) => props.bold && 'font-weight: bold;'} ${(props: Props) => props.italic && 'font-style: italic;'} ${(props: Props) => props.underline && 'text-decoration-line: underline;'} diff --git a/mobile/views/DirectMessageThread/components/DirectMessageThread.js b/mobile/views/DirectMessageThread/components/DirectMessageThread.js new file mode 100644 index 0000000000..dacb54e188 --- /dev/null +++ b/mobile/views/DirectMessageThread/components/DirectMessageThread.js @@ -0,0 +1,110 @@ +// @flow +import React, { Fragment } from 'react'; +import { View } from 'react-native'; +import compose from 'recompose/compose'; +import { Query } from 'react-apollo'; +import Text from '../../../components/Text'; +import ChatInput from '../../../components/ChatInput'; +import Messages from '../../../components/Messages'; +import Avatar from '../../../components/Avatar'; +import Column from '../../../components/Flex/Column'; +import Row from '../../../components/Flex/Row'; +import ViewNetworkHandler, { + type ViewNetworkHandlerProps, +} from '../../../components/ViewNetworkHandler'; + +import sentencify from '../../../../shared/sentencify'; +import getDirectMessageThread, { + type GetDirectMessageThreadType, +} from '../../../../shared/graphql/queries/directMessageThread/getDirectMessageThread'; +import getDirectMessageThreadMessageConnection from '../../../../shared/graphql/queries/directMessageThread/getDirectMessageThreadMessageConnection'; +import type { GetUserType } from '../../../../shared/graphql/queries/user/getUser'; +import sendDirectMessage from '../../../../shared/graphql/mutations/message/sendDirectMessage'; + +import type { DirectMessageThreadInfoType } from '../../../../shared/graphql/fragments/directMessageThread/directMessageThreadInfo'; + +const DirectMessageThreadMessages = getDirectMessageThreadMessageConnection( + Messages +); + +type Props = { + ...$Exact, + id: string, + sendDirectMessage: Function, + currentUser: GetUserType, + data: { + directMessageThread?: GetDirectMessageThreadType, + }, +}; + +class DirectMessageThread extends React.Component { + sendMessage = text => { + if (!this.props.data.directMessageThread) return; + this.props.sendDirectMessage({ + threadId: this.props.data.directMessageThread.id, + threadType: 'directMessageThread', + messageType: 'text', + content: { + body: text, + }, + }); + }; + + render() { + const { + isLoading, + hasError, + data: { directMessageThread }, + currentUser, + } = this.props; + + if (directMessageThread) { + const participants = directMessageThread.participants.filter( + ({ userId }) => userId !== currentUser.id + ); + return ( + + ( + + + {participants.map(({ profilePhoto, id }) => ( + + ))} + + + {sentencify(participants.map(({ name }) => name))} + + + )} + /> + + + ); + } + + if (isLoading) return Loading...; + if (hasError) return Error :(; + return null; + } +} + +export default compose( + ViewNetworkHandler, + sendDirectMessage, + getDirectMessageThread +)(DirectMessageThread); diff --git a/mobile/views/DirectMessageThread/index.js b/mobile/views/DirectMessageThread/index.js new file mode 100644 index 0000000000..e26a1bf390 --- /dev/null +++ b/mobile/views/DirectMessageThread/index.js @@ -0,0 +1,48 @@ +// @flow +import React from 'react'; +import compose from 'recompose/compose'; +import idx from 'idx'; +import Text from '../../components/Text'; +import ViewNetworkHandler, { + type ViewNetworkHandlerProps, +} from '../../components/ViewNetworkHandler'; + +import { + getCurrentUser, + type GetUserType, +} from '../../../shared/graphql/queries/user/getUser'; + +import DirectMessageThread from './components/DirectMessageThread'; +import { Wrapper } from './style'; + +type Props = { + ...$Exact, + data: { + user?: GetUserType, + }, + navigation?: { + state: { + params: { + id: string, + }, + }, + }, +}; + +class DirectMessageThreadView extends React.Component { + render() { + const id = idx(this.props, props => props.navigation.state.params.id); + if (!id) return Non-existant DM thread; + if (!this.props.data.user) return null; + return ( + + {/* TODO(@mxstbr): We have to pass currentUser here because otherwise the sendDirectMessage mutation doesn't work. We should not make that a requirement. */} + + + ); + } +} + +export default compose(ViewNetworkHandler, getCurrentUser)( + DirectMessageThreadView +); diff --git a/mobile/views/DirectMessageThread/style.js b/mobile/views/DirectMessageThread/style.js new file mode 100644 index 0000000000..f2c3081388 --- /dev/null +++ b/mobile/views/DirectMessageThread/style.js @@ -0,0 +1,7 @@ +// @flow +import styled from 'styled-components/native'; + +export const Wrapper = styled.View` + background-color: ${props => props.theme.bg.default}; + flex: 1; +`; diff --git a/mobile/views/DirectMessages/components/DirectMessageThreadListItem.js b/mobile/views/DirectMessages/components/DirectMessageThreadListItem.js new file mode 100644 index 0000000000..41e8f1cc40 --- /dev/null +++ b/mobile/views/DirectMessages/components/DirectMessageThreadListItem.js @@ -0,0 +1,82 @@ +// @flow +import React, { Fragment } from 'react'; +import { TouchableOpacity } from 'react-native'; +import styled, { css } from 'styled-components/native'; +import Text from '../../../components/Text'; +import Avatar from '../../../components/Avatar'; +import Row from '../../../components/Flex/Row'; +import Column from '../../../components/Flex/Column'; +import ConditionalWrap from '../../../components/ConditionalWrap'; +import sentencify from '../../../../shared/sentencify'; +import { timeDifference } from '../../../../shared/time-difference'; + +import type { DirectMessageThreadInfoType } from '../../../../shared/graphql/fragments/directMessageThread/directMessageThreadInfo'; + +const Wrapper = styled(Row)` + padding: 16px 8px; + border-bottom-color: ${props => props.theme.bg.border}; + border-bottom-width: 1px; +`; + +const AvatarWrapper = styled(Column)` + margin-right: 8px; + justify-content: center; + width: 60px; + align-items: center; +`; + +type Props = { + thread: DirectMessageThreadInfoType, + currentUserId: string, + onPress?: Function, +}; + +const DirectMessageThreadListItem = ({ + thread, + currentUserId, + onPress, +}: Props) => { + const participants = thread.participants.filter( + ({ userId }) => userId !== currentUserId + ); + return ( + ( + {children} + )} + > + + + {participants.map(({ profilePhoto, id }) => ( + + ))} + + + + + {sentencify(participants.map(({ name }) => name))} + + props.theme.text.alt}> + {timeDifference(Date.now(), new Date(thread.threadLastActive))} + + + props.theme.text.alt} + > + {thread.snippet} + + + + + ); +}; + +export default DirectMessageThreadListItem; diff --git a/mobile/views/DirectMessages/components/DirectMessageThreadsList.js b/mobile/views/DirectMessages/components/DirectMessageThreadsList.js new file mode 100644 index 0000000000..40671e4b05 --- /dev/null +++ b/mobile/views/DirectMessages/components/DirectMessageThreadsList.js @@ -0,0 +1,64 @@ +// @flow +import React, { Fragment } from 'react'; +import compose from 'recompose/compose'; +import { withNavigation } from 'react-navigation'; +import InfiniteList from '../../../components/InfiniteList'; +import Text from '../../../components/Text'; +import ViewNetworkHandler, { + type ViewNetworkHandlerProps, +} from '../../../components/ViewNetworkHandler'; + +import DirectMessageThreadListItem from './DirectMessageThreadListItem'; + +import { getCurrentUserQuery } from '../../../../shared/graphql/queries/user/getUser'; +import getCurrentUserDMThreadConnection, { + type GetCurrentUserDMThreadConnectionType, +} from '../../../../shared/graphql/queries/directMessageThread/getCurrentUserDMThreadConnection'; +import type { ApolloQueryResult } from 'apollo-client'; +import type { NavigationProps } from 'react-navigation'; + +type Props = { + ...$Exact, + navigation: NavigationProps, + data: { + fetchMore: Function, + user?: $Exact, + }, +}; + +const DirectMessageThreadsList = (props: Props) => { + const { isLoading, hasError, data: { user } } = props; + if (user) { + const { pageInfo, edges } = user.directMessageThreadsConnection; + return ( + + ( + + props.navigation.navigate('DirectMessageThread', { + id: thread.id, + }) + } + /> + )} + hasNextPage={pageInfo.hasNextPage} + fetchMore={props.data.fetchMore} + loadingIndicator={Loading...} + /> + + ); + } + if (isLoading) return Loading...; + if (hasError) return Error; + return No DM Threads yet; +}; + +export default compose( + ViewNetworkHandler, + getCurrentUserDMThreadConnection, + withNavigation +)(DirectMessageThreadsList); diff --git a/mobile/views/DirectMessages/index.js b/mobile/views/DirectMessages/index.js new file mode 100644 index 0000000000..d682359c76 --- /dev/null +++ b/mobile/views/DirectMessages/index.js @@ -0,0 +1,16 @@ +// @flow +import React, { Fragment } from 'react'; +import DirectMessageThreadsList from './components/DirectMessageThreadsList'; +import { Wrapper } from './style'; + +class DirectMessages extends React.Component<{}> { + render() { + return ( + + + + ); + } +} + +export default DirectMessages; diff --git a/mobile/views/DirectMessages/style.js b/mobile/views/DirectMessages/style.js new file mode 100644 index 0000000000..f2c3081388 --- /dev/null +++ b/mobile/views/DirectMessages/style.js @@ -0,0 +1,7 @@ +// @flow +import styled from 'styled-components/native'; + +export const Wrapper = styled.View` + background-color: ${props => props.theme.bg.default}; + flex: 1; +`; diff --git a/mobile/views/TabBar/DirectMessageStack.js b/mobile/views/TabBar/DirectMessageStack.js new file mode 100644 index 0000000000..89c49d74df --- /dev/null +++ b/mobile/views/TabBar/DirectMessageStack.js @@ -0,0 +1,33 @@ +// @flow +import * as React from 'react'; +import { Button } from 'react-native'; +import { StackNavigator } from 'react-navigation'; +import idx from 'idx'; +import BaseStack from './BaseStack'; +import DirectMessages from '../DirectMessages'; +import DirectMessageThread from '../DirectMessageThread'; + +const DMStack = StackNavigator( + { + DirectMessages: { + screen: DirectMessages, + navigationOptions: ({ navigation }) => ({ + headerTitle: + idx(navigation, _ => _.state.params.title) || 'Direct Messages', + }), + }, + DirectMessageThread: { + screen: DirectMessageThread, + navigationOptions: ({ navigation }) => ({ + headerTitle: + idx(navigation, _ => _.state.params.title) || 'Direct Message Thread', + }), + }, + ...BaseStack, + }, + { + initialRouteName: 'DirectMessages', + } +); + +export default DMStack; diff --git a/mobile/views/TabBar/index.js b/mobile/views/TabBar/index.js index c0fcaf13da..d63ec7cd5b 100644 --- a/mobile/views/TabBar/index.js +++ b/mobile/views/TabBar/index.js @@ -7,6 +7,7 @@ import theme from '../../components/theme'; import HomeStack from './HomeStack'; import ProfileStack from './ProfileStack'; import NotificationsStack from './NotificationsStack'; +import DMStack from './DirectMessageStack'; import { ExploreIcon, HomeIcon, @@ -23,7 +24,7 @@ const routeConfiguration = { }, }, Messages: { - screen: HomeStack, + screen: DMStack, navigationOptions: { tabBarIcon: ({ tintColor }) => , }, diff --git a/mobile/views/Thread/components/Byline.js b/mobile/views/Thread/components/Byline.js index e447664060..e0767ad795 100644 --- a/mobile/views/Thread/components/Byline.js +++ b/mobile/views/Thread/components/Byline.js @@ -3,6 +3,8 @@ import React from 'react'; import styled from 'styled-components/native'; import Avatar from '../../../components/Avatar'; import Text from '../../../components/Text'; +import Row from '../../../components/Flex/Row'; +import Column from '../../../components/Flex/Column'; import compose from 'recompose/compose'; import type { Navigation } from '../../../utils/types'; import type { ThreadParticipantType } from '../../../../shared/graphql/fragments/thread/threadParticipant'; @@ -14,14 +16,6 @@ const BylineWrapper = styled.View` width: 100%; `; -const Row = styled.View` - flex-direction: row; -`; - -const Column = styled.View` - flex-direction: column; -`; - type Props = { author: ThreadParticipantType, navigation: Navigation, diff --git a/package.json b/package.json index 5370559291..e1ab1aa0fa 100644 --- a/package.json +++ b/package.json @@ -109,6 +109,7 @@ "history": "^4.6.1", "hoist-non-react-statics": "^2.3.1", "hpp": "^0.2.2", + "idx": "^2.3.0", "imgix-core-js": "^1.0.6", "ioredis": "3.1.4", "isomorphic-fetch": "^2.2.1", diff --git a/shared/clients/group-messages.js b/shared/clients/group-messages.js index 75cf814605..93b9916930 100644 --- a/shared/clients/group-messages.js +++ b/shared/clients/group-messages.js @@ -15,6 +15,7 @@ export const sortAndGroupMessages = (messages: Array) => { // on the first message, get the user id and set it to be checked against const robo = [ { + id: messages[i].timestamp, author: { user: { id: 'robo', diff --git a/shared/graphql/fragments/directMessageThread/directMessageThreadInfo.js b/shared/graphql/fragments/directMessageThread/directMessageThreadInfo.js index bb71ff6418..ef5972b339 100644 --- a/shared/graphql/fragments/directMessageThread/directMessageThreadInfo.js +++ b/shared/graphql/fragments/directMessageThread/directMessageThreadInfo.js @@ -14,8 +14,8 @@ type Participant = { export type DirectMessageThreadInfoType = { id: string, - snippet: ?string, - threadLastActive: ?Date, + threadLastActive: Date, + snippet: string, participants: Array, }; diff --git a/shared/graphql/mutations/message/sendDirectMessage.js b/shared/graphql/mutations/message/sendDirectMessage.js index 36c5f9697b..c2c6546ffa 100644 --- a/shared/graphql/mutations/message/sendDirectMessage.js +++ b/shared/graphql/mutations/message/sendDirectMessage.js @@ -5,6 +5,7 @@ import { btoa } from 'abab'; import messageInfoFragment from '../../fragments/message/messageInfo'; import type { MessageInfoType } from '../../fragments/message/messageInfo'; import { getDMThreadMessageConnectionQuery } from '../../queries/directMessageThread/getDirectMessageThreadMessageConnection'; +import { getCurrentUserQuery } from '../../queries/user/getUser'; export type SendDirectMessageType = { ...$Exact, @@ -19,7 +20,7 @@ export const sendDirectMessageMutation = gql` ${messageInfoFragment} `; const sendDirectMessageOptions = { - props: ({ ownProps, mutate }) => ({ + props: ({ ownProps, mutate, ...rest }) => ({ sendDirectMessage: message => { const fakeId = Math.round(Math.random() * -1000000); return mutate({ @@ -74,7 +75,7 @@ const sendDirectMessageOptions = { const data = store.readQuery({ query: getDMThreadMessageConnectionQuery, variables: { - id: ownProps.thread, + id: ownProps.thread || ownProps.id, }, }); diff --git a/shared/graphql/queries/directMessageThread/getDirectMessageThreadMessageConnection.js b/shared/graphql/queries/directMessageThread/getDirectMessageThreadMessageConnection.js index 869fdd2ebb..fbb81b8232 100644 --- a/shared/graphql/queries/directMessageThread/getDirectMessageThreadMessageConnection.js +++ b/shared/graphql/queries/directMessageThread/getDirectMessageThreadMessageConnection.js @@ -51,6 +51,8 @@ export const getDMThreadMessageConnectionOptions = { directMessageThread && directMessageThread.messageConnection && directMessageThread.messageConnection.edges, + messageConnection: + directMessageThread && directMessageThread.messageConnection, hasNextPage: directMessageThread && directMessageThread.messageConnection ? directMessageThread.messageConnection.pageInfo.hasNextPage diff --git a/shared/time-difference.js b/shared/time-difference.js index c58db53bad..fa5a4e0abf 100644 --- a/shared/time-difference.js +++ b/shared/time-difference.js @@ -1,35 +1,79 @@ -/** - * This file is shared between server and client. - * ⚠️ DON'T PUT ANY NODE.JS OR BROWSER-SPECIFIC CODE IN HERE ⚠️ - * - * Note: This uses Flow comment syntax so this whole file is actually valid JS without any transpilation - * The reason I did that is because create-react-app doesn't transpile files outside the source folder, - * so it chokes on the Flow syntax. - * More info: https://flow.org/en/docs/types/comments/ - */ - -var MS_PER_SECOND = 1000; -var MS_PER_MINUTE = 60000; -var MS_PER_HOUR = 3600000; -var MS_PER_DAY = 86400000; -var MS_PER_YEAR = 31536000000; - -function timeDifferenceShort(current /*: Date*/, previous /*: Date*/) { - var elapsed = current - previous; - - if (elapsed < MS_PER_MINUTE) { - return Math.round(elapsed / MS_PER_SECOND) + 's'; - } else if (elapsed < MS_PER_HOUR) { - return Math.round(elapsed / msPerMinute) + 'm'; - } else if (elapsed < MS_PER_DAY) { - return Math.round(elapsed / MS_PER_HOUR) + 'h'; - } else if (elapsed < MS_PER_YEAR) { - return Math.round(elapsed / MS_PER_DAY) + 'd'; - } else { - return Math.round(elapsed / MS_PER_YEAR) + 'y'; - } -} - -module.exports = { - short: timeDifferenceShort, -}; +// @flow +const MS_PER_SECOND = 1000; +const MS_PER_MINUTE = 60000; +const MS_PER_HOUR = 3600000; +const MS_PER_DAY = 86400000; +const MS_PER_YEAR = 31536000000; + +export function timeDifferenceShort(current: Date, previous: Date) { + const elapsed = current - previous; + + if (elapsed < MS_PER_MINUTE) { + return Math.round(elapsed / MS_PER_SECOND) + 's'; + } else if (elapsed < MS_PER_HOUR) { + return Math.round(elapsed / MS_PER_MINUTE) + 'm'; + } else if (elapsed < MS_PER_DAY) { + return Math.round(elapsed / MS_PER_HOUR) + 'h'; + } else if (elapsed < MS_PER_YEAR) { + return Math.round(elapsed / MS_PER_DAY) + 'd'; + } else { + return Math.round(elapsed / MS_PER_YEAR) + 'y'; + } +} + +export function timeDifference(current: number, previous: ?number): string { + if (!previous) return ''; + + const msPerMinute = 60 * 1000; + const msPerHour = msPerMinute * 60; + const msPerDay = msPerHour * 24; + const msPerMonth = msPerDay * 30; + const msPerYear = msPerDay * 365; + + let elapsed = current - previous; + + if (elapsed < msPerMinute) { + return 'Just now'; + } else if (elapsed < msPerHour) { + const now = Math.round(elapsed / msPerMinute); + if (now === 1) { + return '1 minute ago'; + } else { + return `${now} minutes ago`; + } + } else if (elapsed < msPerDay) { + const now = Math.round(elapsed / msPerHour); + if (now === 1) { + return '1 hour ago'; + } else { + return `${now} hours ago`; + } + } else if (elapsed < msPerMonth) { + const now = Math.round(elapsed / msPerDay); + if (now === 1) { + return 'Yesterday'; + } else if (now >= 7 && now <= 13) { + return '1 week ago'; + } else if (now >= 14 && now <= 20) { + return '2 weeks ago'; + } else if (now >= 21 && now <= 28) { + return '3 weeks ago'; + } else { + return `${now} days ago`; + } + } else if (elapsed < msPerYear) { + const now = Math.round(elapsed / msPerMonth); + if (now === 1) { + return '1 month ago'; + } else { + return `${now} months ago`; + } + } else { + const now = Math.round(elapsed / msPerYear); + if (now === 1) { + return '1 year ago'; + } else { + return `${now} years ago`; + } + } +} diff --git a/src/helpers/utils.js b/src/helpers/utils.js index a095af8224..225e12bb10 100644 --- a/src/helpers/utils.js +++ b/src/helpers/utils.js @@ -86,90 +86,6 @@ export function isMobile() { return false; } -export function timeDifference(current: number, previous: ?number): string { - if (!previous) return ''; - - const msPerMinute = 60 * 1000; - const msPerHour = msPerMinute * 60; - const msPerDay = msPerHour * 24; - const msPerMonth = msPerDay * 30; - const msPerYear = msPerDay * 365; - - let elapsed = current - previous; - - if (elapsed < msPerMinute) { - return 'Just now'; - } else if (elapsed < msPerHour) { - const now = Math.round(elapsed / msPerMinute); - if (now === 1) { - return '1 minute ago'; - } else { - return `${now} minutes ago`; - } - } else if (elapsed < msPerDay) { - const now = Math.round(elapsed / msPerHour); - if (now === 1) { - return '1 hour ago'; - } else { - return `${now} hours ago`; - } - } else if (elapsed < msPerMonth) { - const now = Math.round(elapsed / msPerDay); - if (now === 1) { - return 'Yesterday'; - } else if (now >= 7 && now <= 13) { - return '1 week ago'; - } else if (now >= 14 && now <= 20) { - return '2 weeks ago'; - } else if (now >= 21 && now <= 28) { - return '3 weeks ago'; - } else { - return `${now} days ago`; - } - } else if (elapsed < msPerYear) { - const now = Math.round(elapsed / msPerMonth); - if (now === 1) { - return '1 month ago'; - } else { - return `${now} months ago`; - } - } else { - const now = Math.round(elapsed / msPerYear); - if (now === 1) { - return '1 year ago'; - } else { - return `${now} years ago`; - } - } -} - -export function timeDifferenceShort(current: Date, previous: Date) { - const msPerSecond = 1000; - const msPerMinute = 60 * 1000; - const msPerHour = msPerMinute * 60; - const msPerDay = msPerHour * 24; - const msPerYear = msPerDay * 365; - - let elapsed = current - previous; - - if (elapsed < msPerMinute) { - const now = Math.round(elapsed / msPerSecond); - return `${now}s`; - } else if (elapsed < msPerHour) { - const now = Math.round(elapsed / msPerMinute); - return `${now}m`; - } else if (elapsed < msPerDay) { - const now = Math.round(elapsed / msPerHour); - return `${now}h`; - } else if (elapsed < msPerYear) { - const now = Math.round(elapsed / msPerDay); - return `${now}d`; - } else { - const now = Math.round(elapsed / msPerYear); - return `${now}y`; - } -} - export const debounce = (func: Function, wait: number, immediate: boolean) => { let timeout; return function() { diff --git a/src/views/communitySettings/components/slack/sendInvitations.js b/src/views/communitySettings/components/slack/sendInvitations.js index a69a71e4ac..82df55ddd5 100644 --- a/src/views/communitySettings/components/slack/sendInvitations.js +++ b/src/views/communitySettings/components/slack/sendInvitations.js @@ -13,7 +13,7 @@ import { Button } from 'src/components/buttons'; import { TextArea, Error } from 'src/components/formElements'; import sendSlackInvitesMutation from 'shared/graphql/mutations/community/sendSlackInvites'; import { addToastWithTimeout } from 'src/actions/toasts'; -import { timeDifference } from 'src/helpers/utils'; +import { timeDifference } from 'shared/time-difference'; import Icon from 'src/components/icons'; import type { Dispatch } from 'redux'; diff --git a/src/views/directMessages/components/messageThreadListItem.js b/src/views/directMessages/components/messageThreadListItem.js index 1b5d29741e..6c40a332d7 100644 --- a/src/views/directMessages/components/messageThreadListItem.js +++ b/src/views/directMessages/components/messageThreadListItem.js @@ -1,7 +1,7 @@ import React, { Component } from 'react'; // $FlowFixMe import Link from 'src/components/link'; -import { timeDifference } from '../../../helpers/utils'; +import { timeDifference } from 'shared/time-difference'; import { renderAvatars } from './avatars'; import { Wrapper, diff --git a/src/views/notifications/utils.js b/src/views/notifications/utils.js index 28a62f6220..e5d1ccd31d 100644 --- a/src/views/notifications/utils.js +++ b/src/views/notifications/utils.js @@ -1,6 +1,6 @@ import React from 'react'; import Link from 'src/components/link'; -import { timeDifferenceShort } from '../../helpers/utils'; +import { timeDifferenceShort } from 'shared/time-difference'; import { Timestamp } from './style'; export const parseNotification = notification => { diff --git a/src/views/thread/components/threadDetail.js b/src/views/thread/components/threadDetail.js index ca6ff9bd31..686e39b725 100644 --- a/src/views/thread/components/threadDetail.js +++ b/src/views/thread/components/threadDetail.js @@ -6,9 +6,9 @@ import { withRouter } from 'react-router'; import Link from 'src/components/link'; import { getLinkPreviewFromUrl, - timeDifference, convertTimestampToDate, } from '../../../helpers/utils'; +import { timeDifference } from 'shared/time-difference'; import isURL from 'validator/lib/isURL'; import { URLS } from '../../../helpers/regexps'; import { openModal } from '../../../actions/modals'; diff --git a/yarn.lock b/yarn.lock index b9a132064b..b2d60ad0ed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5643,6 +5643,10 @@ icss-utils@^2.1.0: dependencies: postcss "^6.0.1" +idx@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/idx/-/idx-2.3.0.tgz#4ae3fe3fca4c1baeccf2dde83d9d8b50b47cc465" + ieee754@^1.1.4: version "1.1.11" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.11.tgz#c16384ffe00f5b7835824e67b6f2bd44a5229455"