Start work on Compose page
This commit is contained in:
parent
38a2dfad91
commit
5e679e5a6b
|
@ -1159,6 +1159,15 @@
|
||||||
"integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==",
|
"integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@types/emoji-mart": {
|
||||||
|
"version": "2.8.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/emoji-mart/-/emoji-mart-2.8.4.tgz",
|
||||||
|
"integrity": "sha512-C3J+8nwZxAkgb06+835suFCutyFyz3G4aUFGMlJTvl0ANmE8hj8HUfL92GXNRkJspw+8ciXB6OuwscFFPA56iA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/events": {
|
"@types/events": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
|
||||||
|
@ -5602,6 +5611,15 @@
|
||||||
"minimalistic-crypto-utils": "^1.0.0"
|
"minimalistic-crypto-utils": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"emoji-mart": {
|
||||||
|
"version": "2.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-mart/-/emoji-mart-2.11.0.tgz",
|
||||||
|
"integrity": "sha512-ol1ABg0xfDwCeKVu0Mmr/iTRHUMvqlyLZJFRQy+e6aWV83/Y+TEbJpBbtk7NxaW3BLAOyJ8r0NWthRQwxTtT1A==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"prop-types": "^15.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"emoji-regex": {
|
"emoji-regex": {
|
||||||
"version": "7.0.3",
|
"version": "7.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
|
||||||
|
@ -6786,6 +6804,12 @@
|
||||||
"escape-string-regexp": "^1.0.5"
|
"escape-string-regexp": "^1.0.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"file-dialog": {
|
||||||
|
"version": "0.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/file-dialog/-/file-dialog-0.0.7.tgz",
|
||||||
|
"integrity": "sha512-eRo8DZbV7G0ADtaUPvGAb5ZXB+jN1ehVeYanIHDt2wEokjiXTy5kzMvtiPw4nEtt08IxJqqMeeFZNP34XLNjRw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"file-entry-cache": {
|
"file-entry-cache": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz",
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
"@types/react-dom": "16.8.3",
|
"@types/react-dom": "16.8.3",
|
||||||
"@types/react-router-dom": "^4.3.1",
|
"@types/react-router-dom": "^4.3.1",
|
||||||
"@types/react-swipeable-views": "latest",
|
"@types/react-swipeable-views": "latest",
|
||||||
|
"@types/emoji-mart": "^2.8.2",
|
||||||
"megalodon": "^0.6.0",
|
"megalodon": "^0.6.0",
|
||||||
"moment": "^2.24.0",
|
"moment": "^2.24.0",
|
||||||
"react": "^16.8.6",
|
"react": "^16.8.6",
|
||||||
|
@ -21,7 +22,9 @@
|
||||||
"typescript": "3.3.4000",
|
"typescript": "3.3.4000",
|
||||||
"notistack": "^0.5.1",
|
"notistack": "^0.5.1",
|
||||||
"react-web-share-api": "^0.0.2",
|
"react-web-share-api": "^0.0.2",
|
||||||
"query-string": "^6.4.2"
|
"query-string": "^6.4.2",
|
||||||
|
"file-dialog": "^0.0.7",
|
||||||
|
"emoji-mart": "^2.8.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "BROWSER='Safari Technology Preview' react-scripts start",
|
"start": "BROWSER='Safari Technology Preview' react-scripts start",
|
||||||
|
|
|
@ -14,6 +14,7 @@ import PublicPage from './pages/Public';
|
||||||
import Conversation from './pages/Conversation';
|
import Conversation from './pages/Conversation';
|
||||||
import NotificationsPage from './pages/Notifications';
|
import NotificationsPage from './pages/Notifications';
|
||||||
import SearchPage from './pages/Search';
|
import SearchPage from './pages/Search';
|
||||||
|
import Composer from './pages/Compose';
|
||||||
|
|
||||||
import {withSnackbar} from 'notistack';
|
import {withSnackbar} from 'notistack';
|
||||||
let theme = setHyperspaceTheme(getUserDefaultTheme());
|
let theme = setHyperspaceTheme(getUserDefaultTheme());
|
||||||
|
@ -53,6 +54,7 @@ class App extends Component<any, any> {
|
||||||
<Route path="/search" component={SearchPage}/>
|
<Route path="/search" component={SearchPage}/>
|
||||||
<Route path="/settings" component={Settings}/>
|
<Route path="/settings" component={Settings}/>
|
||||||
<Route path="/about" component={AboutPage}/>
|
<Route path="/about" component={AboutPage}/>
|
||||||
|
<Route path="/compose" component={Composer}/>
|
||||||
</MuiThemeProvider>
|
</MuiThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -142,5 +142,11 @@ export const styles = (theme: Theme) => createStyles({
|
||||||
marginLeft: 250
|
marginLeft: 250
|
||||||
},
|
},
|
||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
|
},
|
||||||
|
composeButton: {
|
||||||
|
position: "fixed",
|
||||||
|
bottom: theme.spacing.unit * 2,
|
||||||
|
right: theme.spacing.unit * 2,
|
||||||
|
zIndex: 50
|
||||||
}
|
}
|
||||||
});
|
});
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { Typography, AppBar, Toolbar, IconButton, InputBase, Avatar, ListItemText, Divider, List, ListItem, ListItemIcon, Hidden, Drawer, ListSubheader, ListItemAvatar, withStyles, Menu, MenuItem, ClickAwayListener, Badge } from '@material-ui/core';
|
import { Typography, AppBar, Toolbar, IconButton, InputBase, Avatar, ListItemText, Divider, List, ListItemIcon, Hidden, Drawer, ListSubheader, ListItemAvatar, withStyles, Menu, MenuItem, ClickAwayListener, Badge } from '@material-ui/core';
|
||||||
import MenuIcon from '@material-ui/icons/Menu';
|
import MenuIcon from '@material-ui/icons/Menu';
|
||||||
import SearchIcon from '@material-ui/icons/Search';
|
import SearchIcon from '@material-ui/icons/Search';
|
||||||
import NotificationsIcon from '@material-ui/icons/Notifications';
|
import NotificationsIcon from '@material-ui/icons/Notifications';
|
||||||
|
@ -10,11 +10,12 @@ import PublicIcon from '@material-ui/icons/Public';
|
||||||
import GroupIcon from '@material-ui/icons/Group';
|
import GroupIcon from '@material-ui/icons/Group';
|
||||||
import SettingsIcon from '@material-ui/icons/Settings';
|
import SettingsIcon from '@material-ui/icons/Settings';
|
||||||
import InfoIcon from '@material-ui/icons/Info';
|
import InfoIcon from '@material-ui/icons/Info';
|
||||||
|
import EditIcon from '@material-ui/icons/Edit';
|
||||||
import SupervisedUserCircleIcon from '@material-ui/icons/SupervisedUserCircle';
|
import SupervisedUserCircleIcon from '@material-ui/icons/SupervisedUserCircle';
|
||||||
import ExitToAppIcon from '@material-ui/icons/ExitToApp';
|
import ExitToAppIcon from '@material-ui/icons/ExitToApp';
|
||||||
import {styles} from './AppLayout.styles';
|
import {styles} from './AppLayout.styles';
|
||||||
import { UAccount } from '../../types/Account';
|
import { UAccount } from '../../types/Account';
|
||||||
import {LinkableListItem, LinkableIconButton} from '../../interfaces/overrides';
|
import {LinkableListItem, LinkableIconButton, LinkableFab} from '../../interfaces/overrides';
|
||||||
import Mastodon from 'megalodon';
|
import Mastodon from 'megalodon';
|
||||||
import { Notification } from '../../types/Notification';
|
import { Notification } from '../../types/Notification';
|
||||||
import {sendNotificationRequest} from '../../utilities/notifications';
|
import {sendNotificationRequest} from '../../utilities/notifications';
|
||||||
|
@ -287,6 +288,9 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
||||||
</Hidden>
|
</Hidden>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
<LinkableFab to="/compose" className={classes.composeButton} color="secondary" aria-label="Compose">
|
||||||
|
<EditIcon/>
|
||||||
|
</LinkableFab>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@ export const styles = (theme: Theme) => createStyles({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
postEmoji: {
|
postEmoji: {
|
||||||
width: theme.typography.body2.fontSize
|
height: theme.typography.fontSize
|
||||||
},
|
},
|
||||||
postMedia: {
|
postMedia: {
|
||||||
height: 0,
|
height: 0,
|
||||||
|
@ -76,5 +76,9 @@ export const styles = (theme: Theme) => createStyles({
|
||||||
postTags: {
|
postTags: {
|
||||||
paddingTop: theme.spacing.unit,
|
paddingTop: theme.spacing.unit,
|
||||||
paddingBottom: theme.spacing.unit
|
paddingBottom: theme.spacing.unit
|
||||||
|
},
|
||||||
|
postAuthorEmoji: {
|
||||||
|
height: theme.typography.fontSize,
|
||||||
|
verticalAlign: "middle"
|
||||||
}
|
}
|
||||||
});
|
});
|
|
@ -19,12 +19,12 @@ import { Tag } from '../../types/Tag';
|
||||||
import { Mention } from '../../types/Mention';
|
import { Mention } from '../../types/Mention';
|
||||||
import { Visibility } from '../../types/Visibility';
|
import { Visibility } from '../../types/Visibility';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { MastodonEmoji } from '../../types/Emojis';
|
|
||||||
import AttachmentComponent from '../Attachment';
|
import AttachmentComponent from '../Attachment';
|
||||||
import Mastodon from 'megalodon';
|
import Mastodon from 'megalodon';
|
||||||
import { LinkableChip, LinkableMenuItem, LinkableIconButton } from '../../interfaces/overrides';
|
import { LinkableChip, LinkableMenuItem, LinkableIconButton } from '../../interfaces/overrides';
|
||||||
import {withSnackbar} from 'notistack';
|
import {withSnackbar} from 'notistack';
|
||||||
import ShareMenu from './PostShareMenu';
|
import ShareMenu from './PostShareMenu';
|
||||||
|
import {emojifyString} from '../../utilities/emojis';
|
||||||
|
|
||||||
interface IPostProps {
|
interface IPostProps {
|
||||||
post: Status;
|
post: Status;
|
||||||
|
@ -61,6 +61,7 @@ export class Post extends React.Component<any, IPostState> {
|
||||||
|
|
||||||
materializeContent(status: Status) {
|
materializeContent(status: Status) {
|
||||||
const { classes } = this.props;
|
const { classes } = this.props;
|
||||||
|
|
||||||
const oldContent = document.createElement('div');
|
const oldContent = document.createElement('div');
|
||||||
oldContent.innerHTML = status.content;
|
oldContent.innerHTML = status.content;
|
||||||
|
|
||||||
|
@ -72,12 +73,8 @@ export class Post extends React.Component<any, IPostState> {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (status.emojis !== undefined && status.emojis.length > 0) {
|
oldContent.innerHTML = emojifyString(oldContent.innerHTML, status.emojis, classes.postEmoji);
|
||||||
status.emojis.forEach((emoji: MastodonEmoji) => {
|
|
||||||
let regexp = new RegExp(':' + emoji.shortcode + ':', 'g');
|
|
||||||
oldContent.innerHTML = oldContent.innerHTML.replace(regexp, `<img src="${emoji.static_url}" class="${classes.postEmoji}"/>`)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<CardContent className={classes.postContent}>
|
<CardContent className={classes.postContent}>
|
||||||
<div className={classes.mediaContainer}>
|
<div className={classes.mediaContainer}>
|
||||||
|
@ -144,12 +141,17 @@ export class Post extends React.Component<any, IPostState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
getReblogAuthors(post: Status) {
|
getReblogAuthors(post: Status) {
|
||||||
|
const { classes } = this.props;
|
||||||
if (post.reblog) {
|
if (post.reblog) {
|
||||||
let author = post.reblog.account;
|
let author = post.reblog.account;
|
||||||
return `${author.display_name || author.username} (@${author.acct}) 🔄 ${post.account.display_name || post.account.username}`
|
let origString = `<span>${author.display_name || author.username} (@${author.acct}) 🔄 ${post.account.display_name || post.account.username}</span>`;
|
||||||
|
let emojis = author.emojis;
|
||||||
|
emojis.concat(post.account.emojis);
|
||||||
|
return emojifyString(origString, emojis, classes.postAuthorEmoji);
|
||||||
} else {
|
} else {
|
||||||
let author = post.account;
|
let author = post.account;
|
||||||
return `${author.display_name || author.username} (@${author.acct})`
|
let origString = `<span>${author.display_name || author.username} (@${author.acct})</span>`;
|
||||||
|
return emojifyString(origString, author.emojis, classes.postAuthorEmoji);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -299,7 +301,10 @@ export class Post extends React.Component<any, IPostState> {
|
||||||
<IconButton key={`${post.id}_submenu`} id={`${post.id}_submenu`} onClick={() => this.togglePostMenu()}>
|
<IconButton key={`${post.id}_submenu`} id={`${post.id}_submenu`} onClick={() => this.togglePostMenu()}>
|
||||||
<MoreVertIcon />
|
<MoreVertIcon />
|
||||||
</IconButton>}
|
</IconButton>}
|
||||||
title={this.getReblogAuthors(post)} subheader={moment(post.created_at).format("MMMM Do YYYY [at] h:mm A")} />
|
title={
|
||||||
|
<Typography dangerouslySetInnerHTML={{__html: this.getReblogAuthors(post)}}></Typography>
|
||||||
|
}
|
||||||
|
subheader={moment(post.created_at).format("MMMM Do YYYY [at] h:mm A")} />
|
||||||
{
|
{
|
||||||
post.reblog? this.getReblogOfPost(post.reblog): null
|
post.reblog? this.getReblogOfPost(post.reblog): null
|
||||||
}
|
}
|
||||||
|
@ -315,7 +320,7 @@ export class Post extends React.Component<any, IPostState> {
|
||||||
}
|
}
|
||||||
<CardActions>
|
<CardActions>
|
||||||
<Tooltip title="Reply">
|
<Tooltip title="Reply">
|
||||||
<LinkableIconButton to={`/compose?reply=${post.id}`}>
|
<LinkableIconButton to={`/compose?reply=${post.reblog? post.reblog.id: post.id}&visibility=${post.visibility}&acct=${post.reblog? post.reblog.account.acct: post.account.acct}`}>
|
||||||
<ReplyIcon/>
|
<ReplyIcon/>
|
||||||
</LinkableIconButton>
|
</LinkableIconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
@ -6,7 +6,6 @@ import * as serviceWorker from './serviceWorker';
|
||||||
import {createUserDefaults, getUserDefaultBool} from './utilities/settings';
|
import {createUserDefaults, getUserDefaultBool} from './utilities/settings';
|
||||||
import {refreshUserAccountData} from './utilities/accounts';
|
import {refreshUserAccountData} from './utilities/accounts';
|
||||||
import {SnackbarProvider} from 'notistack';
|
import {SnackbarProvider} from 'notistack';
|
||||||
import {getNotificationRequestPermission} from './utilities/notifications';
|
|
||||||
|
|
||||||
createUserDefaults();
|
createUserDefaults();
|
||||||
refreshUserAccountData();
|
refreshUserAccountData();
|
||||||
|
@ -16,7 +15,7 @@ ReactDOM.render(
|
||||||
<SnackbarProvider
|
<SnackbarProvider
|
||||||
anchorOrigin={{
|
anchorOrigin={{
|
||||||
vertical: 'bottom',
|
vertical: 'bottom',
|
||||||
horizontal: 'right',
|
horizontal: 'left',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<App />
|
<App />
|
||||||
|
|
|
@ -6,6 +6,7 @@ import Chip, { ChipProps } from '@material-ui/core/Chip';
|
||||||
import { MenuItemProps } from '@material-ui/core/MenuItem';
|
import { MenuItemProps } from '@material-ui/core/MenuItem';
|
||||||
import { MenuItem } from '@material-ui/core';
|
import { MenuItem } from '@material-ui/core';
|
||||||
import Button, { ButtonProps } from '@material-ui/core/Button';
|
import Button, { ButtonProps } from '@material-ui/core/Button';
|
||||||
|
import Fab, { FabProps } from '@material-ui/core/Fab';
|
||||||
|
|
||||||
export interface ILinkableListItemProps extends ListItemProps {
|
export interface ILinkableListItemProps extends ListItemProps {
|
||||||
to: string;
|
to: string;
|
||||||
|
@ -32,6 +33,11 @@ export interface ILinkableButtonProps extends ButtonProps {
|
||||||
replace?: boolean;
|
replace?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ILinkableFabProps extends FabProps {
|
||||||
|
to: string;
|
||||||
|
replace?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export const LinkableListItem = (props: ILinkableListItemProps) => (
|
export const LinkableListItem = (props: ILinkableListItemProps) => (
|
||||||
<ListItem {...props} component={Link as any}/>
|
<ListItem {...props} component={Link as any}/>
|
||||||
)
|
)
|
||||||
|
@ -52,6 +58,10 @@ export const LinkableButton = (props: ILinkableButtonProps) => (
|
||||||
<Button {...props} component={Link as any}/>
|
<Button {...props} component={Link as any}/>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const LinkableFab = (props: ILinkableFabProps) => (
|
||||||
|
<Fab {...props} component={Link as any}/>
|
||||||
|
)
|
||||||
|
|
||||||
export const ProfileRoute = (rest: any, component: Component) => (
|
export const ProfileRoute = (rest: any, component: Component) => (
|
||||||
<Route {...rest} render={props => (
|
<Route {...rest} render={props => (
|
||||||
<Component {...props}/>
|
<Component {...props}/>
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { Theme, createStyles } from "@material-ui/core";
|
||||||
|
|
||||||
|
export const styles = (theme: Theme) => createStyles({
|
||||||
|
dialog: {
|
||||||
|
minHeight: 400
|
||||||
|
},
|
||||||
|
dialogContent: {
|
||||||
|
paddingBottom: 0
|
||||||
|
},
|
||||||
|
dialogActions: {
|
||||||
|
paddingLeft: theme.spacing.unit * 1.25
|
||||||
|
},
|
||||||
|
charsReachingLimit: {
|
||||||
|
color: theme.palette.error.main
|
||||||
|
},
|
||||||
|
warningCaption: {
|
||||||
|
height: 16,
|
||||||
|
verticalAlign: "text-bottom"
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,215 @@
|
||||||
|
import React, {Component} from 'react';
|
||||||
|
import { Dialog, DialogContent, DialogActions, withStyles, Button, CardHeader, Avatar, TextField, Toolbar, IconButton, Fade, Typography, Tooltip, Menu, MenuItem } from '@material-ui/core';
|
||||||
|
import {styles} from './Compose.styles';
|
||||||
|
import { UAccount } from '../types/Account';
|
||||||
|
import { Visibility } from '../types/Visibility';
|
||||||
|
import CameraAltIcon from '@material-ui/icons/CameraAlt';
|
||||||
|
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 Mastodon from 'megalodon';
|
||||||
|
import {withSnackbar} from 'notistack';
|
||||||
|
import { Attachment } from '../types/Attachment';
|
||||||
|
import { PollWizard } from '../types/Poll';
|
||||||
|
import filedialog from 'file-dialog';
|
||||||
|
|
||||||
|
|
||||||
|
interface IComposerState {
|
||||||
|
account: UAccount;
|
||||||
|
visibility: Visibility;
|
||||||
|
sensitive: boolean;
|
||||||
|
sensitiveText?: string;
|
||||||
|
visibilityMenu: boolean;
|
||||||
|
text: string;
|
||||||
|
remainingChars: number;
|
||||||
|
reply?: string;
|
||||||
|
acct?: string;
|
||||||
|
attachments?: [Attachment];
|
||||||
|
poll?: PollWizard;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Composer extends Component<any, IComposerState> {
|
||||||
|
|
||||||
|
client: Mastodon;
|
||||||
|
|
||||||
|
constructor(props: any) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.client = new Mastodon(localStorage.getItem('access_token') as string, localStorage.getItem('baseurl') + "/api/v1");
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
account: JSON.parse(localStorage.getItem('account') as string),
|
||||||
|
visibility: "public",
|
||||||
|
sensitive: false,
|
||||||
|
visibilityMenu: false,
|
||||||
|
text: '',
|
||||||
|
remainingChars: 500
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTextFromField(text: string) {
|
||||||
|
this.setState({ text, remainingChars: 500 - text.length });
|
||||||
|
}
|
||||||
|
|
||||||
|
updateWarningFromField(sensitiveText: string) {
|
||||||
|
this.setState({ sensitiveText });
|
||||||
|
}
|
||||||
|
|
||||||
|
changeVisibility(visibility: Visibility) {
|
||||||
|
this.setState({ visibility });
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadMedia() {
|
||||||
|
filedialog({
|
||||||
|
multiple: false,
|
||||||
|
accept: "image/*, video/*"
|
||||||
|
}).then((media: FileList) => {
|
||||||
|
let mediaForm = new FormData();
|
||||||
|
mediaForm.append('file', media[0]);
|
||||||
|
const uploading = this.props.enqueueSnackbar("Uploading media...", { persist: true })
|
||||||
|
this.client.post('/media', mediaForm).then((resp: any) => {
|
||||||
|
let attachment: Attachment = resp.data;
|
||||||
|
let attachments = this.state.attachments;
|
||||||
|
if (attachments) {
|
||||||
|
attachments.push(attachment);
|
||||||
|
} else {
|
||||||
|
attachments = [attachment];
|
||||||
|
}
|
||||||
|
this.setState({ attachments });
|
||||||
|
this.props.closeSnackbar(uploading);
|
||||||
|
this.props.enqueueSnackbar('Media uploaded.');
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
this.props.enqueueSnackbar("Couldn't upload media: " + err.name);
|
||||||
|
})
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
this.props.enqueueSnackbar("Couldn't get media: " + err.name);
|
||||||
|
console.error(err.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getOnlyMediaIds() {
|
||||||
|
let ids: string[] = [];
|
||||||
|
if (this.state.attachments) {
|
||||||
|
this.state.attachments.map((attachment: Attachment) => {
|
||||||
|
ids.push(attachment.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
post() {
|
||||||
|
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
|
||||||
|
}).then(() => {
|
||||||
|
this.props.enqueueSnackbar('Posted!');
|
||||||
|
window.history.back();
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
this.props.enqueueSnackbar("Couldn't post: " + err.name);
|
||||||
|
console.log(err.message);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSensitive() {
|
||||||
|
this.setState({ sensitive: !this.state.sensitive });
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleVisibilityMenu() {
|
||||||
|
this.setState({ visibilityMenu: !this.state.visibilityMenu });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {classes} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={true} maxWidth="sm" fullWidth={true} className={classes.dialog} onClose={() => window.history.back()}>
|
||||||
|
<CardHeader
|
||||||
|
avatar={
|
||||||
|
<Avatar src={this.state.account.avatar_static} />
|
||||||
|
}
|
||||||
|
title={`${this.state.account.display_name} (@${this.state.account.acct})`}
|
||||||
|
subheader={this.state.visibility.charAt(0).toUpperCase() + this.state.visibility.substr(1)}
|
||||||
|
/>
|
||||||
|
<DialogContent className={classes.dialogContent}>
|
||||||
|
{
|
||||||
|
this.state.sensitive?
|
||||||
|
<Fade in={this.state.sensitive}>
|
||||||
|
<TextField
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
label="Content warning"
|
||||||
|
margin="dense"
|
||||||
|
onChange={(event) => this.updateWarningFromField(event.target.value)}
|
||||||
|
></TextField>
|
||||||
|
</Fade>: null
|
||||||
|
}
|
||||||
|
{
|
||||||
|
this.state.visibility === "direct"?
|
||||||
|
<Typography variant="caption" >
|
||||||
|
<WarningIcon className={classes.warningCaption}/> Don't forget to add the usernames of the accounts you want to message in your post.
|
||||||
|
</Typography>: null
|
||||||
|
}
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
variant="outlined"
|
||||||
|
multiline
|
||||||
|
fullWidth
|
||||||
|
placeholder="What's on your mind?"
|
||||||
|
margin="normal"
|
||||||
|
onChange={(event) => this.updateTextFromField(event.target.value)}
|
||||||
|
inputProps = {
|
||||||
|
{
|
||||||
|
maxLength: 500
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Typography variant="caption" className={this.state.remainingChars <= 100? classes.charsReachingLimit: null}>
|
||||||
|
{`${this.state.remainingChars} character${this.state.remainingChars === 1? '': 's'} remaining`}
|
||||||
|
</Typography>
|
||||||
|
</DialogContent>
|
||||||
|
<Toolbar className={classes.dialogActions}>
|
||||||
|
<Tooltip title="Add photos or videos">
|
||||||
|
<IconButton disabled={this.state.poll !== undefined} onClick={() => this.uploadMedia()} id="compose-media">
|
||||||
|
<CameraAltIcon/>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="Insert emoji">
|
||||||
|
<IconButton id="compose-emoji">
|
||||||
|
<TagFacesIcon/>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="Add a poll">
|
||||||
|
<IconButton disabled={this.state.attachments && this.state.attachments.length > 0} id="compose-poll">
|
||||||
|
<HowToVoteIcon/>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="Change who sees your post">
|
||||||
|
<IconButton id="compose-visibility" onClick={() => this.toggleVisibilityMenu()}>
|
||||||
|
<VisibilityIcon/>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="Set a content warning">
|
||||||
|
<IconButton onClick={() => this.toggleSensitive()} id="compose-warning">
|
||||||
|
<WarningIcon/>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Menu open={this.state.visibilityMenu} anchorEl={document.getElementById('compose-visibility')} onClose={() => this.toggleVisibilityMenu()}>
|
||||||
|
<MenuItem onClick={() => this.changeVisibility('direct')}>Direct (direct message)</MenuItem>
|
||||||
|
<MenuItem onClick={() => this.changeVisibility('private')}>Private (followers only)</MenuItem>
|
||||||
|
<MenuItem onClick={() => this.changeVisibility('unlisted')}>Unlisted</MenuItem>
|
||||||
|
<MenuItem onClick={() => this.changeVisibility('public')}>Public</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</Toolbar>
|
||||||
|
<DialogActions>
|
||||||
|
<Button color="secondary" onClick={() => this.post()}>Post</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withStyles(styles)(withSnackbar(Composer));
|
|
@ -98,6 +98,9 @@ export const styles = (theme: Theme) => createStyles({
|
||||||
marginBottom: theme.spacing.unit,
|
marginBottom: theme.spacing.unit,
|
||||||
backgroundColor: theme.palette.primary.main
|
backgroundColor: theme.palette.primary.main
|
||||||
},
|
},
|
||||||
|
pageProfileNameEmoji: {
|
||||||
|
height: theme.typography.h4.fontSize,
|
||||||
|
},
|
||||||
pageProfileStatsDiv: {
|
pageProfileStatsDiv: {
|
||||||
display: 'inline-flex',
|
display: 'inline-flex',
|
||||||
marginTop: theme.spacing.unit * 2,
|
marginTop: theme.spacing.unit * 2,
|
||||||
|
@ -153,5 +156,5 @@ export const styles = (theme: Theme) => createStyles({
|
||||||
top: 116,
|
top: 116,
|
||||||
right: theme.spacing.unit * 24,
|
right: theme.spacing.unit * 24,
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
|
@ -8,6 +8,7 @@ import { Relationship } from '../types/Relationship';
|
||||||
import Post from '../components/Post';
|
import Post from '../components/Post';
|
||||||
import {withSnackbar} from 'notistack';
|
import {withSnackbar} from 'notistack';
|
||||||
import { LinkableButton } from '../interfaces/overrides';
|
import { LinkableButton } from '../interfaces/overrides';
|
||||||
|
import { emojifyString } from '../utilities/emojis';
|
||||||
|
|
||||||
interface IProfilePageState {
|
interface IProfilePageState {
|
||||||
account?: Account;
|
account?: Account;
|
||||||
|
@ -210,7 +211,7 @@ class ProfilePage extends Component<any, IProfilePageState> {
|
||||||
<div className={classes.pageHeroBackgroundImage} style={{ backgroundImage: this.state.account? `url("${this.state.account.header}")`: `url("")`}}/>
|
<div className={classes.pageHeroBackgroundImage} style={{ backgroundImage: this.state.account? `url("${this.state.account.header}")`: `url("")`}}/>
|
||||||
<div className={classes.pageHeroContent}>
|
<div className={classes.pageHeroContent}>
|
||||||
<Avatar className={classes.pageProfileAvatar} src={this.state.account ? this.state.account.avatar: ""}/>
|
<Avatar className={classes.pageProfileAvatar} src={this.state.account ? this.state.account.avatar: ""}/>
|
||||||
<Typography variant="h4" color="inherit">{this.state.account ? this.state.account.display_name: ""}</Typography>
|
<Typography variant="h4" color="inherit" dangerouslySetInnerHTML={{__html: this.state.account? emojifyString(this.state.account.display_name, this.state.account.emojis, classes.pageProfileNameEmoji): ""}}></Typography>
|
||||||
<Typography variant="caption" color="inherit">{this.state.account ? '@' + this.state.account.acct: ""}</Typography>
|
<Typography variant="caption" color="inherit">{this.state.account ? '@' + this.state.account.acct: ""}</Typography>
|
||||||
<Typography paragraph color="inherit">{this.state.account ? this.state.account.note: ""}</Typography>
|
<Typography paragraph color="inherit">{this.state.account ? this.state.account.note: ""}</Typography>
|
||||||
<Divider/>
|
<Divider/>
|
||||||
|
|
|
@ -17,4 +17,14 @@ export type Poll = {
|
||||||
export type PollOption = {
|
export type PollOption = {
|
||||||
title: string;
|
title: string;
|
||||||
votes_count: number | null;
|
votes_count: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PollWizard = {
|
||||||
|
expires_at: string;
|
||||||
|
multiple: boolean;
|
||||||
|
options: PollWizardOption[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PollWizardOption = {
|
||||||
|
title: string;
|
||||||
}
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
import {MastodonEmoji} from '../types/Emojis';
|
||||||
|
|
||||||
|
// if (status.emojis !== undefined && status.emojis.length > 0) {
|
||||||
|
// status.emojis.forEach((emoji: MastodonEmoji) => {
|
||||||
|
// let regexp = new RegExp(':' + emoji.shortcode + ':', 'g');
|
||||||
|
// oldContent.innerHTML = oldContent.innerHTML.replace(regexp, `<img src="${emoji.static_url}" class="${classes.postEmoji}"/>`)
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes a given string and replaces emoji codes with their respective image tags.
|
||||||
|
* @param contents The string to replace with emojis
|
||||||
|
* @param emojis The set of emojis to replace the content with
|
||||||
|
* @param className The associated class for the string
|
||||||
|
* @returns String with image tags for emojis
|
||||||
|
*/
|
||||||
|
export function emojifyString(contents: string, emojis: [MastodonEmoji], className?: any): string {
|
||||||
|
let newContents: string = contents;
|
||||||
|
|
||||||
|
emojis.forEach((emoji: MastodonEmoji) => {
|
||||||
|
let filter = new RegExp(`:${emoji.shortcode}:`, 'g');
|
||||||
|
newContents = newContents.replace(filter, `<img src=${emoji.static_url} ${className? `class="${className}"`: ""}/>`)
|
||||||
|
})
|
||||||
|
|
||||||
|
return newContents;
|
||||||
|
}
|
Loading…
Reference in New Issue