Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhancements #240, #233 Comments reply and other actions #253

Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,26 @@ function writeThreadcapChunk(processedNodeId, threadcap, sentCommenters, res) {
res.write(JSON.stringify(threadcapChunk) + '\n')
}

// ---------------------------------------------------------
// --------- API to get remote interact url for comments ---
// ---------------------------------------------------------
app.use('/api/comments/remoteInteractUrlPattern', async (req, res) => {
const interactorAccount = req.query.interactorAccount;

console.log('Debug interactorAccount', interactorAccount);

const interactorInstanceHost = interactorAccount.split('@')[1];

const response = await fetch(`https://${interactorInstanceHost}/.well-known/webfinger?` + new URLSearchParams({resource:`acct:${interactorAccount}`}));
const parsedResponse = await response.json();

const linkOStatusSubscribe = parsedResponse.links.find((link) => link.rel === 'http://ostatus.org/schema/1.0/subscribe');

res.send({
remoteInteractUrlPattern: linkOStatusSubscribe.template
})
})

// ------------------------------------------------
// ---------- Static files for API data -----------
// ------------------------------------------------
Expand Down
129 changes: 129 additions & 0 deletions ui/src/components/CommentMenu/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import * as React from 'react'

import './styles.scss'

interface ICommentMenuProps {
/**
* URL of the comment
*/
url: string,
commenterUrl: string,
commenterAccount: string,
}

interface ICommentMenuState {
interactorAccount?: string,
}

class CommentMenu extends React.PureComponent<ICommentMenuProps, ICommentMenuState> {
constructor(props) {
super(props);
this.state = {
interactorAccount: localStorage.getItem('commentsInteractorAccount')
}
}

onClickCopyLink(e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) {
e.preventDefault();
navigator.clipboard.writeText(this.props.url);

// Some form of "toast message" would be better, but I don't
// think something like that is already implemented, meanwhile,
// we use an alert, it is ugly, but it show the user there was
// a reaction.
alert('Link to post copied');
}

async onClickReply(e: React.MouseEvent<HTMLAnchorElement, MouseEvent>): Promise<void> {
e.preventDefault();

const remoteInteractUrlPattern = await this.fetchRemoteInteractUrlPattern();

if(!remoteInteractUrlPattern) {
// TODO: error handling
}

// alert(remoteInteractUrlPattern);
const remoteInteractUrl = remoteInteractUrlPattern.replace('{uri}', encodeURI(this.props.url));
window.open(remoteInteractUrl, '_blank');
}

async onClickFollow(e: React.MouseEvent<HTMLAnchorElement, MouseEvent>): Promise<void> {
e.preventDefault();

const remoteInteractUrlPattern = await this.fetchRemoteInteractUrlPattern();

if(!remoteInteractUrlPattern) {
// TODO: error handling
}

// alert(remoteInteractUrlPattern);
const remoteInteractUrl = remoteInteractUrlPattern.replace('{uri}', encodeURI(CommentMenu.stripLeadingAt(this.props.commenterAccount)));
window.open(remoteInteractUrl, '_blank');
}

async fetchRemoteInteractUrlPattern(): Promise<string> {
let interactorAccount = localStorage.getItem('commentsInteractorAccount');

if(!interactorAccount) {
interactorAccount = prompt('What is your Fediverse account?');

if(interactorAccount) {
localStorage.setItem('commentsInteractorAccount', interactorAccount);
}
}

if(!interactorAccount) {
return;
}

let remoteInteractUrlPattern = localStorage.getItem('commentsRemoteInteractUrlPattern');

if(!remoteInteractUrlPattern) {
const response = await fetch('/api/comments/remoteInteractUrlPattern?' + new URLSearchParams({
interactorAccount: interactorAccount
}));

const parsedResponse = await response.json();

remoteInteractUrlPattern = parsedResponse.remoteInteractUrlPattern;

if(remoteInteractUrlPattern) {
localStorage.setItem('commentsRemoteInteractUrlPattern', remoteInteractUrlPattern);
}
}

return remoteInteractUrlPattern;
}

static stripLeadingAt(account: string) {
return account.substring(1);
}

onClickForgetHomeInstanceHost(e: React.MouseEvent<HTMLAnchorElement, MouseEvent>): void {
e.preventDefault();

localStorage.removeItem('commentsInteractorAccount');
localStorage.removeItem('commentsRemoteInteractUrlPattern');

this.setState({
interactorAccount: undefined
});
}

render(): React.ReactNode {
return (
<menu>
<a href={this.props.url} target='_blank' onClick={(e) => this.onClickReply(e)}>Reply to this post</a>
<a href={this.props.url} onClick={(e) => this.onClickCopyLink(e)}>Copy link to this post</a>
<a href={this.props.url} target='_blank'>Open in original site</a>
<a href={this.props.commenterUrl} target='_blank' onClick={(e) => this.onClickFollow(e)}>Follow {this.props.commenterAccount}</a>
{this.state.interactorAccount &&
<a onClick={(e) => this.onClickForgetHomeInstanceHost(e)}>Forget {this.state.interactorAccount}</a>
}
</menu>
)
}
}

export default CommentMenu;
84 changes: 84 additions & 0 deletions ui/src/components/CommentMenu/styles.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
.context-menu {
all: unset;
position: relative;
}

.context-menu svg {
display: block;
}

.context-menu menu {
position: absolute;
top: 100%;
right: 0;
display: flex;
flex-direction: column;
background: var(--button-background);
margin: 0;
padding: .75rem;
z-index: 1;
border-radius: .25rem;
box-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.25);
}

.context-menu menu a {
white-space: nowrap;
text-decoration: none;
line-height: 2;
}

.dialog-homeinstance {
border: 1px solid rgba(0, 0, 0, 0.25);
border-radius: 0.5rem;
box-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.25);
width: calc((100% - 6px) - 2em);
max-width: 28rem;
}

.dialog-homeinstance form {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin: 0;
}

.dialog-homeinstance input {
padding: 0.5rem;
}

.dialog-homeinstance menu {
display: flex;
flex-wrap: wrap;
flex-direction: row;
justify-content: flex-end;
gap: .5rem;
margin: 1.5rem 0 0 0;
}

.dialog-homeinstance button {
border: none;
padding: .5rem;
border-radius: 4px;
min-width: 4.5rem;
background-color: #2962ff;
font-size: 0.875rem;
font-weight: 500;
letter-spacing: .25px;
line-height: 1rem;
outline: none;
color: #FFF;
}

.dialog-homeinstance button:hover {
background-color: #2F7DE2;
}

.dialog-homeinstance button[type="reset"] {
background-color: transparent;
box-shadow: inset 0 0 0 1px #DADCE0;
color: #2962ff;
}

.dialog-homeinstance button[type="reset"]:hover {
background-color: #EAF2FD;
}
27 changes: 25 additions & 2 deletions ui/src/components/Comments/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from 'react'
import DOMPurify from 'dompurify'

import './styles.scss'
import CommentMenu from '../CommentMenu'

interface IProps {
id: number,
Expand Down Expand Up @@ -36,9 +37,20 @@ interface ICommentProps {
comment: StateComment
}

class Comment extends React.PureComponent<ICommentProps> {
interface ICommentState {
showMenu: boolean
}

class Comment extends React.PureComponent<ICommentProps, ICommentState> {
constructor(props) {
super(props);
this.state = {
showMenu: false
}
}

onClickShowMenu() {
this.setState({showMenu: !this.state.showMenu});
}

render(): React.ReactNode {
Expand All @@ -53,10 +65,21 @@ class Comment extends React.PureComponent<ICommentProps> {
<span className='handle'>{this.props.comment.attributedTo.account}</span>
</div>
</a>
<span aria-hidden="true">·</span>
<a href={this.props.comment.url} className='permalink'>
<time>{this.props.comment.publishedAt.toLocaleString()}</time>
</a>
<button className="context-menu" aria-label="More" onClick={() => this.onClickShowMenu()}>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" width="24" height="24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM12.75 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM18.75 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0z" />
dellagustin marked this conversation as resolved.
Show resolved Hide resolved
</svg>
{ this.state.showMenu &&
<CommentMenu
url={this.props.comment.url}
commenterUrl={this.props.comment.attributedTo.url}
commenterAccount={this.props.comment.attributedTo.account}
/>
}
</button>
</summary>
}
{ this.props.comment.summary ?
Expand Down
2 changes: 0 additions & 2 deletions ui/src/components/Comments/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@
font-size: .75rem;
line-height: 1rem;
width: 100%;
overflow: hidden;
white-space: nowrap;
}

details > summary::-webkit-details-marker {
Expand Down