diff --git a/package-lock.json b/package-lock.json index 760b67a..bfafb43 100644 --- a/package-lock.json +++ b/package-lock.json @@ -944,6 +944,21 @@ "integrity": "sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==", "dev": true }, + "@date-io/core": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@date-io/core/-/core-1.1.0.tgz", + "integrity": "sha512-PyjhyR2fbp7Q8xpB5zoOyT3dqr8Bn4kXfREf1w6AnQalwdftNxChB2/p88fI1qsx8KNmHDJY12eCgLoZaPegTw==", + "dev": true + }, + "@date-io/moment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@date-io/moment/-/moment-1.1.0.tgz", + "integrity": "sha512-U7Vrhk7nlrKMtlLuNs7DAWmMsMBTgWFAvgpKg6Z5Opo/U7ab5DvI8nLks5dqJbeEFQDSlO+KCmJWfsL9hVI3Jw==", + "dev": true, + "requires": { + "@date-io/core": "^1.1.0" + } + }, "@material-ui/core": { "version": "3.9.3", "resolved": "https://registry.npmjs.org/@material-ui/core/-/core-3.9.3.tgz", @@ -1290,6 +1305,15 @@ "@types/react": "*" } }, + "@types/react-text-mask": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/@types/react-text-mask/-/react-text-mask-5.4.4.tgz", + "integrity": "sha512-mnwyDgUwFtJVAZ8f+tzPGmYjpH7TLXxHSGty338abca6aAjdjRLCOC4h+CxlvB8xrVAIU5pkNllpHhfJCA3hXQ==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/react-transition-group": { "version": "2.0.16", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-2.0.16.tgz", @@ -4152,6 +4176,12 @@ "shallow-clone": "^0.1.2" } }, + "clsx": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.0.3.tgz", + "integrity": "sha512-xLoSw6DMp7YvbEeLrQJBcWWRRerdHrU1WHoL1hYJOKUeDpVMRq7pv7NI2JHQbCRAe5ptINNzhdYmtfN6MsdCUw==", + "dev": true + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -10806,6 +10836,20 @@ "object-visit": "^1.0.0" } }, + "material-ui-pickers": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/material-ui-pickers/-/material-ui-pickers-2.2.4.tgz", + "integrity": "sha512-QCQh08Ylmnt+o4laW+rPs92QRAcESv3sPXl50YadLm++rAZAXAOh3K8lreGdynCMYFgZfdyu81Oz9xzTlAZNfw==", + "dev": true, + "requires": { + "@types/react-text-mask": "^5.4.3", + "clsx": "^1.0.2", + "react-event-listener": "^0.6.6", + "react-text-mask": "^5.4.3", + "react-transition-group": "^2.5.3", + "tslib": "^1.9.3" + } + }, "math-random": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz", @@ -15315,6 +15359,15 @@ } } }, + "react-text-mask": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/react-text-mask/-/react-text-mask-5.4.3.tgz", + "integrity": "sha1-mR77QpnjDC5sLEbRP2FxaUY+DS0=", + "dev": true, + "requires": { + "prop-types": "^15.5.6" + } + }, "react-transition-group": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.7.1.tgz", diff --git a/package.json b/package.json index 5b0d67f..c99fe79 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,9 @@ "react-web-share-api": "^0.0.2", "query-string": "^6.4.2", "file-dialog": "^0.0.7", - "emoji-mart": "^2.8.2" + "emoji-mart": "^2.8.2", + "material-ui-pickers": "^2.2.4", + "@date-io/moment": "^1.1.0" }, "scripts": { "start": "BROWSER='Safari Technology Preview' react-scripts start", diff --git a/src/components/AppLayout/AppLayout.tsx b/src/components/AppLayout/AppLayout.tsx index cc7b5ab..835e1d8 100644 --- a/src/components/AppLayout/AppLayout.tsx +++ b/src/components/AppLayout/AppLayout.tsx @@ -254,7 +254,7 @@ export class AppLayout extends Component { - Switch account + {/* Switch account */} Log out diff --git a/src/components/Post/Post.styles.tsx b/src/components/Post/Post.styles.tsx index 8f2e6d8..52c524e 100644 --- a/src/components/Post/Post.styles.tsx +++ b/src/components/Post/Post.styles.tsx @@ -80,5 +80,8 @@ export const styles = (theme: Theme) => createStyles({ postAuthorEmoji: { height: theme.typography.fontSize, verticalAlign: "middle" + }, + heading: { + color: "inherit" } }); \ No newline at end of file diff --git a/src/components/Post/Post.tsx b/src/components/Post/Post.tsx index f8126bb..bfa40a8 100644 --- a/src/components/Post/Post.tsx +++ b/src/components/Post/Post.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Typography, IconButton, Card, CardHeader, Avatar, CardContent, CardActions, withStyles, Menu, MenuItem, Chip, Divider, CardMedia, CardActionArea, ExpansionPanel, ExpansionPanelSummary, ExpansionPanelDetails, Zoom, Tooltip } from '@material-ui/core'; +import { Typography, IconButton, Card, CardHeader, Avatar, CardContent, CardActions, withStyles, Menu, MenuItem, Chip, Divider, CardMedia, CardActionArea, ExpansionPanel, ExpansionPanelSummary, ExpansionPanelDetails, Zoom, Tooltip, RadioGroup, Radio, FormControlLabel, Button } from '@material-ui/core'; import MoreVertIcon from '@material-ui/icons/MoreVert'; import ReplyIcon from '@material-ui/icons/Reply'; import FavoriteIcon from '@material-ui/icons/Favorite'; @@ -25,6 +25,7 @@ import { LinkableChip, LinkableMenuItem, LinkableIconButton } from '../../interf import {withSnackbar} from 'notistack'; import ShareMenu from './PostShareMenu'; import {emojifyString} from '../../utilities/emojis'; +import { PollOption, Poll } from '../../types/Poll'; interface IPostProps { post: Status; @@ -36,6 +37,7 @@ interface IPostState { post: Status; media_slides?: number; menuIsOpen: boolean; + myVote?: [number]; } export class Post extends React.Component { @@ -59,6 +61,54 @@ export class Post extends React.Component { this.setState({ menuIsOpen: !this.state.menuIsOpen }) } + findBiggestVote() { + let poll = this.state.post.poll; + let votes: number[] = []; + let bv = ""; + if (poll) { + poll.options.forEach((option: PollOption) => { + votes.push(option.votes_count? option.votes_count: 0); + }); + let biggestVote = Math.max.apply(null, votes); + poll.options.forEach((option: PollOption) => { + if (option.votes_count === biggestVote) { + bv = option.title; + }; + }); + return bv; + } else { + return "No poll option was the best."; + } + } + + captureVote(option: any) { + let poll = this.state.post.poll; + let pollIndex: number = 0; + if (poll) { + poll.options.forEach((pollOption: PollOption, index: number) => { + if (pollOption.title === option) { + pollIndex = index; + } + }) + } + this.setState({ myVote: [pollIndex] }); + } + + submitVote() { + let poll = this.state.post.poll; + if (poll) { + this.client.post(`/polls/${poll.id}/votes`, {choices: this.state.myVote}).then((resp: any) => { + let post = this.state.post; + post.poll = resp.data; + this.setState({ post }); + this.props.enqueueSnackbar("Vote submitted."); + }).catch((err: Error) => { + this.props.enqueueSnackbar("Couldn't vote: " + err.name); + console.error(err.message); + }) + } + } + materializeContent(status: Status) { const { classes } = this.props; @@ -105,22 +155,81 @@ export class Post extends React.Component { : } + { + status.poll? + status.poll.voted || status.poll.expired? +
+ You can't vote on this poll. Below are the results of the poll. + + { + status.poll.options.map((pollOption: PollOption) => { + let x = } + label={`${pollOption.title} (${pollOption.votes_count} votes)`} + key={pollOption.title+pollOption.votes_count} + />; + return (x); + + }) + } + + { + status.poll && status.poll.expired? + This poll has expired.: + This poll will expire on {moment(status.poll.expires_at? status.poll.expires_at: "").format('MMMM Do YYYY, [at] h:mm A')}. + } +
: +
+ this.captureVote(option)} + > + { + status.poll.options.map((pollOption: PollOption) => { + let x = } + label={pollOption.title} + key={pollOption.title+pollOption.votes_count} + />; + return (x); + + }) + } + + +
: null + } ); } + spoilerContainsFlags(text: string): boolean { + let unsafeFlags = ["NSFW", "nsfw", "lewd", "sex"]; + let result: boolean = false; + unsafeFlags.forEach((flag: string) => { + if (text.includes(flag)) { + result = true; + } + }) + return result; + } + getSensitiveContent(spoiler_text: string, content: Status) { const { classes } = this.props; const warningText = spoiler_text || "Unmarked content"; let icon; - if (spoiler_text.includes("NSFW") || spoiler_text.includes("Spoiler") || warningText === "Unmarked content") { + if (this.spoilerContainsFlags(spoiler_text) || spoiler_text.includes("Spoiler") || warningText === "Unmarked content") { icon = ; } return ( - + } color="inherit"> - {icon}{warningText} + {icon}{warningText} {this.materializeContent(content)} diff --git a/src/pages/Compose.styles.tsx b/src/pages/Compose.styles.tsx index eb7038a..c4d12ec 100644 --- a/src/pages/Compose.styles.tsx +++ b/src/pages/Compose.styles.tsx @@ -35,5 +35,14 @@ export const styles = (theme: Theme) => createStyles({ [theme.breakpoints.up('sm')]: { display: "block" } + }, + pollWizardOptionIcon: { + marginRight: theme.spacing.unit * 2, + marginTop: 4, + marginBottom: 4, + color: theme.palette.grey[700] + }, + pollWizardFlexGrow: { + flexGrow: 1 } }); \ No newline at end of file diff --git a/src/pages/Compose.tsx b/src/pages/Compose.tsx index 3a90417..e27b446 100644 --- a/src/pages/Compose.tsx +++ b/src/pages/Compose.tsx @@ -9,14 +9,17 @@ import TagFacesIcon from '@material-ui/icons/TagFaces'; import HowToVoteIcon from '@material-ui/icons/HowToVote'; import VisibilityIcon from '@material-ui/icons/Visibility'; import WarningIcon from '@material-ui/icons/Warning'; +import DeleteIcon from '@material-ui/icons/Delete'; +import RadioButtonCheckedIcon from '@material-ui/icons/RadioButtonChecked'; import Mastodon from 'megalodon'; import {withSnackbar} from 'notistack'; import { Attachment } from '../types/Attachment'; -import { PollWizard } from '../types/Poll'; +import { PollWizard, PollWizardOption } from '../types/Poll'; import filedialog from 'file-dialog'; import ComposeMediaAttachment from '../components/ComposeMediaAttachment'; import EmojiPicker from '../components/EmojiPicker'; - +import { DateTimePicker, MuiPickersUtilsProvider } from 'material-ui-pickers'; +import MomentUtils from '@date-io/moment'; interface IComposerState { account: UAccount; @@ -30,6 +33,7 @@ interface IComposerState { acct?: string; attachments?: [Attachment]; poll?: PollWizard; + pollExpiresDate?: any; showEmojis: boolean; } @@ -200,14 +204,120 @@ class Composer extends Component { } } + createPoll() { + if (this.state.poll === undefined) { + let expiration = new Date(); + let current = new Date(); + expiration.setMinutes(expiration.getMinutes() + 30); + let expiryDifference = (expiration.getTime() - current.getTime() / 1000); + let temporaryPoll: PollWizard = { + expires_at: expiryDifference.toString(), + multiple: false, + options: [{title: 'Option 1'}, {title: 'Option 2'}] + } + this.setState({ + poll: temporaryPoll, + pollExpiresDate: expiration + }); + } + } + + addPollItem() { + if (this.state.poll !== undefined && this.state.poll.options.length < 4) { + let newOption = {title: 'New option'} + let options = this.state.poll.options; + let poll = this.state.poll; + options.push(newOption); + poll.options = options; + poll.multiple = true; + this.setState({ + poll: poll + }) + } else if (this.state.poll && this.state.poll.options.length == 4) { + this.props.enqueueSnackbar("You've reached the options limit in your poll.", { variant: 'error' }) + } + } + + editPollItem(position: number, newTitle: any) { + if (this.state.poll !== undefined) { + let poll = this.state.poll; + let options = this.state.poll.options; + options.forEach((option: PollWizardOption) => { + if (position === options.indexOf(option)) { + option.title = newTitle.target.value; + } + }); + poll.options = options; + this.setState({ + poll: poll + }); + this.props.enqueueSnackbar('Option edited.'); + } + } + + removePollItem(item: string) { + if (this.state.poll !== undefined && this.state.poll.options.length > 2) { + let options = this.state.poll.options; + let poll = this.state.poll; + options.forEach((option: PollWizardOption) => { + if (item === option.title) { + options.splice(options.indexOf(option), 1); + } + }); + poll.options = options; + if (options.length === 2) { + poll.multiple = false; + } + this.setState({ + poll: poll + }) + } else if (this.state.poll && this.state.poll.options.length <= 2) { + this.props.enqueueSnackbar('Polls must have at least two items.', { variant: 'error'} ); + } + } + + setPollExpires(date: string) { + let currentDate = new Date(); + let newDate = new Date(date); + let poll = this.state.poll; + if (poll) { + let expiry = ((newDate.getTime() - currentDate.getTime()) / 1000); + console.log(expiry); + if (expiry >= 1800) { + poll.expires_at = expiry.toString(); + this.setState({ poll, pollExpiresDate: date }); + this.props.enqueueSnackbar("Expiration updated.") + } else { + this.props.enqueueSnackbar("Expiration is too small (min. 30 minutes).", { variant: 'error' }); + } + } + } + + removePoll() { + this.setState({ + poll: undefined + }); + } + post() { + let pollOptions: string[] = []; + if (this.state.poll) { + this.state.poll.options.forEach((option: PollWizardOption) => { + pollOptions.push(option.title); + }) + } this.client.post('/statuses', { status: this.state.text, media_ids: this.getOnlyMediaIds(), visibility: this.state.visibility, sensitive: this.state.sensitive, spoiler_text: this.state.sensitiveText, - in_reply_to_id: this.state.reply + in_reply_to_id: this.state.reply, + poll: this.state.poll? { + options: pollOptions, + expires_in: this.state.poll.expires_at, + multiple: this.state.poll.multiple + }: null }).then(() => { this.props.enqueueSnackbar('Posted!'); window.history.back(); @@ -299,6 +409,44 @@ class Composer extends Component { : null } + { + this.state.poll? +
+ + { + this.state.poll? + this.state.poll.options.map((option: PollWizardOption, index: number) => { + let c =
+ + this.editPollItem(index, event)} + defaultValue={option.title}/> +
+ + this.removePollItem(option.title)}> + + + +
+ return c; + }): null + } +
+ + { + this.setPollExpires(date.toISOString()); + }} + label="Poll exipres on" + disablePast + /> + +
+ +
+
: null + } @@ -320,7 +468,11 @@ class Composer extends Component { this.insertEmoji(emoji)}/> - 0} id="compose-poll"> + 0} id="compose-poll" onClick={() => { + this.state.poll? + this.removePoll(): + this.createPoll() + }}> diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 377b1aa..15dc8dc 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -3,7 +3,7 @@ import { withStyles, CircularProgress, Typography, Paper, Button, Chip, Avatar, import {styles} from './PageLayout.styles'; import Post from '../components/Post'; import { Status } from '../types/Status'; -import Mastodon from 'megalodon'; +import Mastodon, { StreamListener } from 'megalodon'; import {withSnackbar} from 'notistack'; import ArrowUpwardIcon from '@material-ui/icons/ArrowUpward'; @@ -20,7 +20,7 @@ interface IHomePageState { class HomePage extends Component { client: Mastodon; - streamListener: any; + streamListener: StreamListener; constructor(props: any) { super(props); @@ -31,11 +31,11 @@ class HomePage extends Component { } this.client = new Mastodon(localStorage.getItem('access_token') as string, localStorage.getItem('baseurl') as string + "/api/v1"); + this.streamListener = this.client.stream('/streaming/user'); } componentWillMount() { - this.streamListener = this.client.stream('/streaming/user'); this.streamListener.on('connect', () => { this.client.get('/timelines/home', {limit: 40}).then((resp: any) => { @@ -92,6 +92,10 @@ class HomePage extends Component { }) } + componentWillUnmount() { + this.streamListener.stop(); + } + insertBacklog() { window.scrollTo(0, 0); let posts = this.state.posts; diff --git a/src/pages/Local.tsx b/src/pages/Local.tsx index 13ef6ce..a8f7af1 100644 --- a/src/pages/Local.tsx +++ b/src/pages/Local.tsx @@ -3,7 +3,7 @@ import { withStyles, CircularProgress, Typography, Paper, Button, Chip, Avatar, import {styles} from './PageLayout.styles'; import Post from '../components/Post'; import { Status } from '../types/Status'; -import Mastodon from 'megalodon'; +import Mastodon, { StreamListener } from 'megalodon'; import {withSnackbar} from 'notistack'; import ArrowUpwardIcon from '@material-ui/icons/ArrowUpward'; @@ -20,7 +20,7 @@ interface ILocalPageState { class LocalPage extends Component { client: Mastodon; - streamListener: any; + streamListener: StreamListener; constructor(props: any) { super(props); @@ -31,11 +31,11 @@ class LocalPage extends Component { } this.client = new Mastodon(localStorage.getItem('access_token') as string, localStorage.getItem('baseurl') as string + "/api/v1"); + this.streamListener = this.client.stream('/streaming/public/local'); } componentWillMount() { - this.streamListener = this.client.stream('/streaming/public/local'); this.streamListener.on('connect', () => { this.client.get('/timelines/public', {limit: 40, local: true}).then((resp: any) => { @@ -92,6 +92,10 @@ class LocalPage extends Component { }) } + componentWillUnmount() { + this.streamListener.stop(); + } + insertBacklog() { window.scrollTo(0, 0); let posts = this.state.posts; diff --git a/src/pages/Public.tsx b/src/pages/Public.tsx index 57c2c5a..b5f9609 100644 --- a/src/pages/Public.tsx +++ b/src/pages/Public.tsx @@ -3,7 +3,7 @@ import { withStyles, CircularProgress, Typography, Paper, Button, Chip, Avatar, import {styles} from './PageLayout.styles'; import Post from '../components/Post'; import { Status } from '../types/Status'; -import Mastodon from 'megalodon'; +import Mastodon, { StreamListener } from 'megalodon'; import {withSnackbar} from 'notistack'; import ArrowUpwardIcon from '@material-ui/icons/ArrowUpward'; @@ -20,7 +20,7 @@ interface IPublicPageState { class PublicPage extends Component { client: Mastodon; - streamListener: any; + streamListener: StreamListener; constructor(props: any) { super(props); @@ -31,12 +31,11 @@ class PublicPage extends Component { } this.client = new Mastodon(localStorage.getItem('access_token') as string, localStorage.getItem('baseurl') as string + "/api/v1"); + this.streamListener = this.client.stream('/streaming/public'); } componentWillMount() { - this.streamListener = this.client.stream('/streaming/public'); - this.streamListener.on('connect', () => { this.client.get('/timelines/public', {limit: 40}).then((resp: any) => { let statuses: [Status] = resp.data; @@ -92,6 +91,10 @@ class PublicPage extends Component { }) } + componentWillUnmount() { + this.streamListener.stop(); + } + insertBacklog() { window.scrollTo(0, 0); let posts = this.state.posts;