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==",
|
||||
"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": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
|
||||
|
@ -5602,6 +5611,15 @@
|
|||
"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": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
|
||||
|
@ -6786,6 +6804,12 @@
|
|||
"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": {
|
||||
"version": "2.0.0",
|
||||
"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-router-dom": "^4.3.1",
|
||||
"@types/react-swipeable-views": "latest",
|
||||
"@types/emoji-mart": "^2.8.2",
|
||||
"megalodon": "^0.6.0",
|
||||
"moment": "^2.24.0",
|
||||
"react": "^16.8.6",
|
||||
|
@ -21,7 +22,9 @@
|
|||
"typescript": "3.3.4000",
|
||||
"notistack": "^0.5.1",
|
||||
"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": {
|
||||
"start": "BROWSER='Safari Technology Preview' react-scripts start",
|
||||
|
|
|
@ -14,6 +14,7 @@ import PublicPage from './pages/Public';
|
|||
import Conversation from './pages/Conversation';
|
||||
import NotificationsPage from './pages/Notifications';
|
||||
import SearchPage from './pages/Search';
|
||||
import Composer from './pages/Compose';
|
||||
|
||||
import {withSnackbar} from 'notistack';
|
||||
let theme = setHyperspaceTheme(getUserDefaultTheme());
|
||||
|
@ -53,6 +54,7 @@ class App extends Component<any, any> {
|
|||
<Route path="/search" component={SearchPage}/>
|
||||
<Route path="/settings" component={Settings}/>
|
||||
<Route path="/about" component={AboutPage}/>
|
||||
<Route path="/compose" component={Composer}/>
|
||||
</MuiThemeProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -142,5 +142,11 @@ export const styles = (theme: Theme) => createStyles({
|
|||
marginLeft: 250
|
||||
},
|
||||
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 { 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 SearchIcon from '@material-ui/icons/Search';
|
||||
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 SettingsIcon from '@material-ui/icons/Settings';
|
||||
import InfoIcon from '@material-ui/icons/Info';
|
||||
import EditIcon from '@material-ui/icons/Edit';
|
||||
import SupervisedUserCircleIcon from '@material-ui/icons/SupervisedUserCircle';
|
||||
import ExitToAppIcon from '@material-ui/icons/ExitToApp';
|
||||
import {styles} from './AppLayout.styles';
|
||||
import { UAccount } from '../../types/Account';
|
||||
import {LinkableListItem, LinkableIconButton} from '../../interfaces/overrides';
|
||||
import {LinkableListItem, LinkableIconButton, LinkableFab} from '../../interfaces/overrides';
|
||||
import Mastodon from 'megalodon';
|
||||
import { Notification } from '../../types/Notification';
|
||||
import {sendNotificationRequest} from '../../utilities/notifications';
|
||||
|
@ -287,6 +288,9 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
|||
</Hidden>
|
||||
</nav>
|
||||
</div>
|
||||
<LinkableFab to="/compose" className={classes.composeButton} color="secondary" aria-label="Compose">
|
||||
<EditIcon/>
|
||||
</LinkableFab>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ export const styles = (theme: Theme) => createStyles({
|
|||
}
|
||||
},
|
||||
postEmoji: {
|
||||
width: theme.typography.body2.fontSize
|
||||
height: theme.typography.fontSize
|
||||
},
|
||||
postMedia: {
|
||||
height: 0,
|
||||
|
@ -76,5 +76,9 @@ export const styles = (theme: Theme) => createStyles({
|
|||
postTags: {
|
||||
paddingTop: 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 { Visibility } from '../../types/Visibility';
|
||||
import moment from 'moment';
|
||||
import { MastodonEmoji } from '../../types/Emojis';
|
||||
import AttachmentComponent from '../Attachment';
|
||||
import Mastodon from 'megalodon';
|
||||
import { LinkableChip, LinkableMenuItem, LinkableIconButton } from '../../interfaces/overrides';
|
||||
import {withSnackbar} from 'notistack';
|
||||
import ShareMenu from './PostShareMenu';
|
||||
import {emojifyString} from '../../utilities/emojis';
|
||||
|
||||
interface IPostProps {
|
||||
post: Status;
|
||||
|
@ -61,6 +61,7 @@ export class Post extends React.Component<any, IPostState> {
|
|||
|
||||
materializeContent(status: Status) {
|
||||
const { classes } = this.props;
|
||||
|
||||
const oldContent = document.createElement('div');
|
||||
oldContent.innerHTML = status.content;
|
||||
|
||||
|
@ -72,12 +73,8 @@ export class Post extends React.Component<any, IPostState> {
|
|||
}
|
||||
});
|
||||
|
||||
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}"/>`)
|
||||
})
|
||||
}
|
||||
oldContent.innerHTML = emojifyString(oldContent.innerHTML, status.emojis, classes.postEmoji);
|
||||
|
||||
return (
|
||||
<CardContent className={classes.postContent}>
|
||||
<div className={classes.mediaContainer}>
|
||||
|
@ -144,12 +141,17 @@ export class Post extends React.Component<any, IPostState> {
|
|||
}
|
||||
|
||||
getReblogAuthors(post: Status) {
|
||||
const { classes } = this.props;
|
||||
if (post.reblog) {
|
||||
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 {
|
||||
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()}>
|
||||
<MoreVertIcon />
|
||||
</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
|
||||
}
|
||||
|
@ -315,7 +320,7 @@ export class Post extends React.Component<any, IPostState> {
|
|||
}
|
||||
<CardActions>
|
||||
<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/>
|
||||
</LinkableIconButton>
|
||||
</Tooltip>
|
||||
|
|
|
@ -6,7 +6,6 @@ import * as serviceWorker from './serviceWorker';
|
|||
import {createUserDefaults, getUserDefaultBool} from './utilities/settings';
|
||||
import {refreshUserAccountData} from './utilities/accounts';
|
||||
import {SnackbarProvider} from 'notistack';
|
||||
import {getNotificationRequestPermission} from './utilities/notifications';
|
||||
|
||||
createUserDefaults();
|
||||
refreshUserAccountData();
|
||||
|
@ -16,7 +15,7 @@ ReactDOM.render(
|
|||
<SnackbarProvider
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
>
|
||||
<App />
|
||||
|
|
|
@ -6,6 +6,7 @@ import Chip, { ChipProps } from '@material-ui/core/Chip';
|
|||
import { MenuItemProps } from '@material-ui/core/MenuItem';
|
||||
import { MenuItem } from '@material-ui/core';
|
||||
import Button, { ButtonProps } from '@material-ui/core/Button';
|
||||
import Fab, { FabProps } from '@material-ui/core/Fab';
|
||||
|
||||
export interface ILinkableListItemProps extends ListItemProps {
|
||||
to: string;
|
||||
|
@ -32,6 +33,11 @@ export interface ILinkableButtonProps extends ButtonProps {
|
|||
replace?: boolean;
|
||||
}
|
||||
|
||||
export interface ILinkableFabProps extends FabProps {
|
||||
to: string;
|
||||
replace?: boolean;
|
||||
}
|
||||
|
||||
export const LinkableListItem = (props: ILinkableListItemProps) => (
|
||||
<ListItem {...props} component={Link as any}/>
|
||||
)
|
||||
|
@ -52,6 +58,10 @@ export const LinkableButton = (props: ILinkableButtonProps) => (
|
|||
<Button {...props} component={Link as any}/>
|
||||
)
|
||||
|
||||
export const LinkableFab = (props: ILinkableFabProps) => (
|
||||
<Fab {...props} component={Link as any}/>
|
||||
)
|
||||
|
||||
export const ProfileRoute = (rest: any, component: Component) => (
|
||||
<Route {...rest} render={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,
|
||||
backgroundColor: theme.palette.primary.main
|
||||
},
|
||||
pageProfileNameEmoji: {
|
||||
height: theme.typography.h4.fontSize,
|
||||
},
|
||||
pageProfileStatsDiv: {
|
||||
display: 'inline-flex',
|
||||
marginTop: theme.spacing.unit * 2,
|
||||
|
@ -153,5 +156,5 @@ export const styles = (theme: Theme) => createStyles({
|
|||
top: 116,
|
||||
right: theme.spacing.unit * 24,
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
|
@ -8,6 +8,7 @@ import { Relationship } from '../types/Relationship';
|
|||
import Post from '../components/Post';
|
||||
import {withSnackbar} from 'notistack';
|
||||
import { LinkableButton } from '../interfaces/overrides';
|
||||
import { emojifyString } from '../utilities/emojis';
|
||||
|
||||
interface IProfilePageState {
|
||||
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.pageHeroContent}>
|
||||
<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 paragraph color="inherit">{this.state.account ? this.state.account.note: ""}</Typography>
|
||||
<Divider/>
|
||||
|
|
|
@ -17,4 +17,14 @@ export type Poll = {
|
|||
export type PollOption = {
|
||||
title: string;
|
||||
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