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

Quote posts #52

Draft
wants to merge 59 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
c239715
models: status: add support for quoting
kaniini Dec 25, 2022
e059e56
activitypub: note serializer: begrudgingly serialize quotes using mis…
kaniini Dec 25, 2022
56570ed
context helper: add quoteUrl as as:quoteUrl, even though its wrong
kaniini Dec 25, 2022
3b89c3f
activitypub: resolve quoted objects when new create activities are re…
kaniini Dec 25, 2022
fc12711
statuses controller: accept quote_id parameter
kaniini Dec 25, 2022
218d967
services: post status service: add quote_id to status parameters
kaniini Dec 25, 2022
54d9f87
views: add quote status html view
sneakers-the-rat Jun 30, 2024
a9387c7
rest: status serializer: include quote data
kaniini Dec 25, 2022
a235fb0
db: add quote_id to statuses table
kaniini Dec 25, 2022
4e822f0
sanitizer config: add quote-inline span to allowlist
kaniini Dec 25, 2022
5c04898
db: add quote_id migration
kaniini Dec 25, 2022
33ff4ab
status: disallow quoting of non-public posts
kaniini Dec 25, 2022
6dbd045
status: prevent recursion when serializing
kaniini Dec 25, 2022
dff6a02
db: add quote_id index
kaniini Dec 25, 2022
63f0be1
status: support either _misskey_quote or quoteUrl for fetching quotes
kaniini Dec 25, 2022
9825f8f
activitypub: case transform: support _misskey keys without messing th…
sneakers-the-rat Jun 30, 2024
3fbde6d
activitypub: note serializer: support _misskey keys
kaniini Dec 25, 2022
fa87ba7
javascript: glitch: pre-process misskey quotes to remove the URL part
sneakers-the-rat Jun 30, 2024
8a7161b
javascript: glitch: dont render cards if the status has a quote attached
kaniini Dec 25, 2022
993e62f
javascript: glitch: start rendering quotes
kaniini Dec 25, 2022
e4df9d6
flavors: glitch: show emojified display name in quotes
sneakers-the-rat Jun 30, 2024
3ca521a
flavors: glitch: action bar: add quote button
kaniini Dec 26, 2022
7124a90
glitch: actions: add quoteCompose and cancelQuoteCompose
kaniini Dec 26, 2022
e42ffda
flavors: glitch: add quote indicator component
kaniini Dec 26, 2022
96fdd9a
flavors: glitch: add quote handling to status feature
sneakers-the-rat Aug 11, 2024
c9a93d7
add styles for quote indicator
kaniini Dec 26, 2022
6b879de
glitch: reducers: set up correct state for quoting
kaniini Dec 26, 2022
77249c3
glitch: fix up quote indicator
sneakers-the-rat Aug 11, 2024
0e794e5
activitypub: switch to fedibird:quoteUri
kaniini Dec 26, 2022
62f4fcc
formatting helper: add the quote-inline hack for incompatible clients
kaniini Dec 26, 2022
86f0bce
components: detailed status: suppress cards on quote posts
kaniini Dec 26, 2022
0275843
add quote option to detailed statuses
sneakers-the-rat Aug 11, 2024
ec8f5d7
activitypub: fix context extensions for quote_uri
kaniini Dec 26, 2022
93e42dc
add reply and quote icons to the reply/quote indicators so people kno…
kaniini Dec 26, 2022
05d1881
delete obsolete console.log statements
kaniini Dec 26, 2022
64d51e1
status: turn quote author/text into links to original profile/post
arachnist Dec 28, 2022
7d90e94
mimic 5d0ed011915acf294b2f63a01ccc8a90aa07b61b
sneakers-the-rat Jun 30, 2024
27a2225
th: run haml-lint on quote haml
kouhaidev Jul 5, 2023
0002968
th: fix js quote issue
kouhaidev Jul 7, 2023
8c47b24
th: glitch: fix quote-toot icon
sneakers-the-rat Jun 30, 2024
06b656f
th: glitch: status: fix quote icon on quotes
kaniini Feb 3, 2024
c24511c
th: fiddle with quote style
kouhaidev Feb 3, 2024
1522507
rubocop
sneakers-the-rat Jun 30, 2024
54e721b
js and css lint
sneakers-the-rat Jun 30, 2024
2abef98
i18n lint
sneakers-the-rat Aug 11, 2024
9292850
haml lint and updating to treehouses current version
sneakers-the-rat Jun 30, 2024
d6c307a
fix build
sneakers-the-rat Jun 30, 2024
d5dd792
actually a few more branches that should be tested
sneakers-the-rat Jun 30, 2024
7034bed
fix recursive quote foreign key
sneakers-the-rat Jun 30, 2024
057f57a
minor merge bugs - working version
sneakers-the-rat Jun 30, 2024
040b84e
fix i18n
sneakers-the-rat Jul 28, 2024
1088276
mention OP when quoting
sneakers-the-rat Jul 28, 2024
d366e20
move quote to bottom, separate into different component
sneakers-the-rat Jul 28, 2024
32c8753
no idea what it does but it mimics other behavior and makes the tests…
sneakers-the-rat Jul 28, 2024
be23faa
fix links in quotes, separate out CSS
sneakers-the-rat Aug 4, 2024
e1cf667
rm script-src nonce in development bc it prevents relaxing other CSPs…
sneakers-the-rat Aug 11, 2024
ffe2066
fix quote modal to match new upstream style
sneakers-the-rat Aug 11, 2024
bdf152d
fix quoteCompose action also to match new upstream style
sneakers-the-rat Aug 11, 2024
7dd2591
quotable concern stub, i think this is where you put this?
sneakers-the-rat Aug 11, 2024
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
1 change: 1 addition & 0 deletions .github/workflows/test-ruby.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ on:
branches:
- 'main'
- 'stable-*'
- 'merge-upstream'
pull_request:

env:
Expand Down
4 changes: 3 additions & 1 deletion app/controllers/api/v1/statuses_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ def create
content_type: status_params[:content_type],
allowed_mentions: status_params[:allowed_mentions],
idempotency: request.headers['Idempotency-Key'],
with_rate_limit: true
with_rate_limit: true,
quote_id: status_params[:quote_id].presence
)

render json: @status, serializer: serializer_for_status
Expand Down Expand Up @@ -159,6 +160,7 @@ def status_params
:visibility,
:language,
:scheduled_at,
:quote_id,
:content_type,
allowed_mentions: [],
media_ids: [],
Expand Down
1 change: 1 addition & 0 deletions app/helpers/context_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ module ContextHelper
'cipherText' => 'toot:cipherText',
},
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
quote_uri: { 'fedibird' => 'http://fedibird.com/ns#', 'quoteUri' => 'fedibird:quoteUri' },
}.freeze

def full_context
Expand Down
12 changes: 11 additions & 1 deletion app/helpers/formatting_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,17 @@ def extract_status_plain_text(status)
module_function :extract_status_plain_text

def status_content_format(status)
html_aware_format(status.text, status.local?, preloaded_accounts: [status.account] + (status.respond_to?(:active_mentions) ? status.active_mentions.map(&:account) : []), content_type: status.content_type)
base = html_aware_format(status.text, status.local?, preloaded_accounts: [status.account] + (status.respond_to?(:active_mentions) ? status.active_mentions.map(&:account) : []), content_type: status.content_type)

if status.quote? && status.local?
after_html = begin
"<span class=\"quote-inline\"><a href=\"#{status.quote.to_log_permalink}\" class=\"status-link unhandled-link\" target=\"_blank\">#{status.quote.to_log_permalink}</a></span>"
end.html_safe # rubocop:disable Rails/OutputSafety

base + after_html
else
base
end
end

def rss_status_content_format(status)
Expand Down
21 changes: 21 additions & 0 deletions app/javascript/flavours/glitch/actions/compose.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ export const COMPOSE_CHANGE_MEDIA_ORDER = 'COMPOSE_CHANGE_MEDIA_ORDER';
export const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS';
export const COMPOSE_FOCUS = 'COMPOSE_FOCUS';

export const COMPOSE_QUOTE = 'COMPOSE_QUOTE';
export const COMPOSE_QUOTE_CANCEL = 'COMPOSE_QUOTE_CANCEL';

const messages = defineMessages({
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
Expand Down Expand Up @@ -152,6 +155,23 @@ export function cancelReplyCompose() {
};
}

export function quoteCompose(status) {
return (dispatch, getState) => {
dispatch({
type: COMPOSE_QUOTE,
status: status,
});

ensureComposeIsVisible(getState);
};
}

export function cancelQuoteCompose() {
return {
type: COMPOSE_QUOTE_CANCEL,
};
}

export function resetCompose() {
return {
type: COMPOSE_RESET,
Expand Down Expand Up @@ -240,6 +260,7 @@ export function submitCompose(overridePrivacy = null) {
status,
content_type: getState().getIn(['compose', 'content_type']),
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
quote_id: getState().getIn(['compose', 'quote_id'], null),
media_ids: media.map(item => item.get('id')),
media_attributes,
sensitive: getState().getIn(['compose', 'sensitive']) || (spoilerText.length > 0 && media.size !== 0),
Expand Down
31 changes: 31 additions & 0 deletions app/javascript/flavours/glitch/actions/importer/normalizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ export function normalizeStatus(status, normalOldStatus, settings) {
normalStatus.contentHtml = normalOldStatus.get('contentHtml');
normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml');
normalStatus.hidden = normalOldStatus.get('hidden');
normalStatus.quote = normalOldStatus.get('quote');
normalStatus.quote_hidden = normalOldStatus.get('quote_hidden');

if (normalOldStatus.get('translation')) {
normalStatus.translation = normalOldStatus.get('translation');
Expand All @@ -72,6 +74,35 @@ export function normalizeStatus(status, normalOldStatus, settings) {
normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
normalStatus.hidden = (spoilerText.length > 0 || normalStatus.sensitive) && autoHideCW(settings, spoilerText);

if (status.quote && status.quote.id) {
const quote_spoilerText = status.quote.spoiler_text || '';
const quote_searchContent = [quote_spoilerText, status.quote.content].join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');

const quote_emojiMap = makeEmojiMap(normalStatus.quote.emojis);

const quote_account_emojiMap = makeEmojiMap(status.quote.account.emojis);
const displayName = normalStatus.quote.account.display_name.length === 0 ? normalStatus.quote.account.username : normalStatus.quote.account.display_name;
normalStatus.quote.account.display_name_html = emojify(escapeTextContentForBrowser(displayName), quote_account_emojiMap);
normalStatus.quote.search_index = domParser.parseFromString(quote_searchContent, 'text/html').documentElement.textContent;
let docElem = domParser.parseFromString(normalStatus.quote.content, 'text/html').documentElement;
Array.from(docElem.querySelectorAll('span.quote-inline'), span => span.remove());
Array.from(docElem.querySelectorAll('p,br'), line => {
let parentNode = line.parentNode;
if (line.nextSibling) {
parentNode.insertBefore(document.createTextNode(' '), line.nextSibling);
}
});
let _contentHtml = docElem.textContent;
normalStatus.quote.contentHtml = '<p>'+emojify(_contentHtml.slice(0, 150), quote_emojiMap) + (_contentHtml.slice(150) ? '...' : '')+'</p>';
normalStatus.quote.spoilerHtml = emojify(escapeTextContentForBrowser(quote_spoilerText), quote_emojiMap);
normalStatus.quote_hidden = (quote_spoilerText.length > 0 || normalStatus.quote.sensitive) && autoHideCW(settings, quote_spoilerText);

// delete the quote link!!!!
let parentDocElem = domParser.parseFromString(normalStatus.contentHtml, 'text/html').documentElement;
Array.from(parentDocElem.querySelectorAll('span.quote-inline'), span => span.remove());
normalStatus.contentHtml = parentDocElem.children[1].innerHTML;
}
}

if (normalOldStatus) {
Expand Down
1 change: 1 addition & 0 deletions app/javascript/flavours/glitch/api_types/statuses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,5 @@ export interface ApiStatusJSON {
// glitch-soc additions
local_only?: boolean;
content_type?: string;
quotable?: boolean;
}
104 changes: 104 additions & 0 deletions app/javascript/flavours/glitch/components/quote_content.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import PropTypes from "prop-types";
import {useCallback} from "react";

import {defineMessages, injectIntl} from "react-intl";

import ImmutablePropTypes from "react-immutable-proptypes";

import QuoteIcon from "@/material-icons/400-24px/format_quote-fill.svg?react";
import {Avatar} from "flavours/glitch/components/avatar";
import {Icon} from "flavours/glitch/components/icon";
import {RelativeTimestamp} from "flavours/glitch/components/relative_timestamp";

const messages = defineMessages({
edited: {id: 'status.edited', defaultMessage: 'Edited {date}'},
});

const QuoteContent = ({
quoteStatus,
parseClick,
intl
}) => {
let quoteStatusContent = {__html: quoteStatus.get('contentHtml')};
let quoteStatusAccount = quoteStatus.get('account');
let quoteStatusDisplayName = {__html: quoteStatusAccount.get('display_name_html')};
const handle = quoteStatus.getIn(['account', 'acct']);
const accountURL = quoteStatus.getIn(['account', 'url']);
const statusID = quoteStatus.get('id');
const createdAt = quoteStatus.get('created_at');
const editedAt = quoteStatus.get('edited_at');

const handleAccountClick = useCallback((e) => {
parseClick(e, `/@${handle}`);
}, [handle, parseClick]);

const handleStatusClick = useCallback((e) => {
parseClick(e, `/@${handle}/${statusID}`);
}, [handle, statusID, parseClick]);

return (
<div className={"status__quote"} onClick={handleStatusClick}>

Check warning on line 40 in app/javascript/flavours/glitch/components/quote_content.jsx

View workflow job for this annotation

GitHub Actions / lint

Avoid non-native interactive elements. If using native HTML is not possible, add an appropriate role and support for tabbing, mouse, keyboard, and touch inputs to an interactive content element
<blockquote>
<div className={"quote__header"}>
<a href={accountURL} onClick={handleAccountClick}
className={"quote__author"}
data-hover-card-account={quoteStatus.getIn(['account', 'id'])}>
<Icon
fixedWidth
aria-hidden='true'
key='icon-quote-right'
icon={QuoteIcon} />
<Avatar account={quoteStatusAccount} size={24} />
<bdi>
<span className='quote__display-name'>
<strong className={"display-name__html"}
dangerouslySetInnerHTML={quoteStatusDisplayName} />
</span>
</bdi>
<span
className={"quote__account deemphasized"}>
@{handle}
</span>

</a>
<span className={"quote__spacer deemphasized"}>
·
</span>
<a href={quoteStatus.get('url')}
onClick={handleStatusClick}
className={"quote__datetime deemphasized"}>
<RelativeTimestamp timestamp={createdAt} />{editedAt && <abbr
title={intl.formatMessage(messages.edited, {
date: intl.formatDate(editedAt, {
year: 'numeric',
month: 'short',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
})}> *</abbr>}

</a>
</div>
<div>
<a className={"quote__content-link"}
href={quoteStatus.get('url')}
onClick={handleStatusClick}
target='_blank' rel='noopener noreferrer'
>
<p className={"quote__content"} dangerouslySetInnerHTML={quoteStatusContent} />
</a>

</div>
</blockquote>
</div>
);
};

QuoteContent.propTypes = {
quoteStatus: ImmutablePropTypes.map.isRequired,
parseClick: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};

export default injectIntl(QuoteContent);
3 changes: 2 additions & 1 deletion app/javascript/flavours/glitch/components/status.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ class Status extends ImmutablePureComponent {
rootId: PropTypes.string,
onClick: PropTypes.func,
onReply: PropTypes.func,
onQuote: PropTypes.func,
onFavourite: PropTypes.func,
onReblog: PropTypes.func,
onBookmark: PropTypes.func,
Expand Down Expand Up @@ -730,7 +731,7 @@ class Status extends ImmutablePureComponent {
if (!status.get('sensitive') && !(status.get('spoiler_text').length > 0) && settings.getIn(['collapsed', 'backgrounds', 'preview_images'])) {
background = attachments.getIn([0, 'preview_url']);
}
} else if (status.get('card') && settings.get('inline_preview_cards') && !this.props.muted) {
} else if (!status.get('quote') && status.get('card') && settings.get('inline_preview_cards') && !this.props.muted) {
media.push(
<Card
onOpenMedia={this.handleOpenMedia}
Expand Down
30 changes: 30 additions & 0 deletions app/javascript/flavours/glitch/components/status_action_bar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';

import BookmarkIcon from '@/material-icons/400-24px/bookmark-fill.svg?react';
import BookmarkBorderIcon from '@/material-icons/400-24px/bookmark.svg?react';
import FormatQuoteIcon from '@/material-icons/400-24px/format_quote-fill.svg?react';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
Expand Down Expand Up @@ -46,6 +47,7 @@ const messages = defineMessages({
replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
quote: { id: 'status.quote', defaultMessage: 'Quote' },
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
Expand Down Expand Up @@ -74,6 +76,7 @@ class StatusActionBar extends ImmutablePureComponent {
onReply: PropTypes.func,
onFavourite: PropTypes.func,
onReblog: PropTypes.func,
onQuote: PropTypes.func,
onDelete: PropTypes.func,
onDirect: PropTypes.func,
onMention: PropTypes.func,
Expand Down Expand Up @@ -140,6 +143,17 @@ class StatusActionBar extends ImmutablePureComponent {
}
};

handleQuoteClick = () => {
const { signedIn } = this.props.identity;

if (signedIn) {
this.props.onQuote(this.props.status, this.context.router.history);
} else {
// TODO(ariadne): Add an interaction modal for quoting specifically.
this.props.onInteractionModal('reply', this.props.status);
}
};

handleBookmarkClick = (e) => {
this.props.onBookmark(this.props.status, e);
};
Expand Down Expand Up @@ -217,6 +231,7 @@ class StatusActionBar extends ImmutablePureComponent {

let menu = [];
let reblogIcon = 'retweet';
let quoteIcon = 'quote-right';
let replyIcon;
let replyIconComponent;
let replyTitle;
Expand Down Expand Up @@ -299,6 +314,7 @@ class StatusActionBar extends ImmutablePureComponent {
const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private';

let reblogTitle, reblogIconComponent;
let quoteTitle, quoteIconComponent;

if (status.get('reblogged')) {
reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
Expand All @@ -314,6 +330,19 @@ class StatusActionBar extends ImmutablePureComponent {
reblogIconComponent = RepeatDisabledIcon;
}

// quotes
if (publicStatus) {
quoteTitle = intl.formatMessage(messages.quote);
quoteIconComponent = FormatQuoteIcon;
} else if (reblogPrivate) {
quoteTitle = intl.formatMessage(messages.reblog_private);
quoteIconComponent = FormatQuoteIcon;
} else {
quoteTitle = intl.formatMessage(messages.cannot_reblog);
quoteIconComponent = FormatQuoteIcon;
}


const filterButton = this.props.onFilter && (
<IconButton className='status__action-bar-button' title={intl.formatMessage(messages.hide)} icon='eye' iconComponent={VisibilityIcon} onClick={this.handleHideClick} />
);
Expand All @@ -330,6 +359,7 @@ class StatusActionBar extends ImmutablePureComponent {
obfuscateCount
/>
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon={reblogIcon} iconComponent={reblogIconComponent} onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} />
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} /* active={status.get('reblogged')} */ title={quoteTitle} icon={quoteIcon} iconComponent={quoteIconComponent} onClick={this.handleQuoteClick} />
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
<IconButton className='status__action-bar-button bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} />

Expand Down
Loading
Loading