diff --git a/app/javascript/gabsocial/components/panel/media_gallery_panel.js b/app/javascript/gabsocial/components/panel/media_gallery_panel.js
index f5835fcd..f0ff3655 100644
--- a/app/javascript/gabsocial/components/panel/media_gallery_panel.js
+++ b/app/javascript/gabsocial/components/panel/media_gallery_panel.js
@@ -61,7 +61,7 @@ class MediaGalleryPanel extends ImmutablePureComponent {
noPadding
title={intl.formatMessage(messages.title)}
headerButtonTitle={!!account ? intl.formatMessage(messages.show_all) : undefined}
- headerButtonTo={!!account ? `/${account.get('acct')}/media` : undefined}
+ headerButtonTo={!!account ? `/${account.get('acct')}/photos` : undefined}
>
{
diff --git a/app/javascript/gabsocial/components/popover/chat_conversation_expiration_options_popover.js b/app/javascript/gabsocial/components/popover/chat_conversation_expiration_options_popover.js
new file mode 100644
index 00000000..50d7ed63
--- /dev/null
+++ b/app/javascript/gabsocial/components/popover/chat_conversation_expiration_options_popover.js
@@ -0,0 +1,135 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import { connect } from 'react-redux'
+import { defineMessages, injectIntl } from 'react-intl'
+import { closePopover } from '../../actions/popover'
+import { changeExpiresAt } from '../../actions/compose'
+import {
+ EXPIRATION_OPTION_5_MINUTES,
+ EXPIRATION_OPTION_60_MINUTES,
+ EXPIRATION_OPTION_6_HOURS,
+ EXPIRATION_OPTION_24_HOURS,
+ EXPIRATION_OPTION_3_DAYS,
+ EXPIRATION_OPTION_7_DAYS,
+} from '../../constants'
+import PopoverLayout from './popover_layout'
+import List from '../list'
+
+class ChatConversationExpirationOptionsPopover extends React.PureComponent {
+
+ handleOnSetExpiration = (expiresAt) => {
+ this.props.onChangeExpiresAt(expiresAt)
+ this.handleOnClosePopover()
+ }
+
+ handleOnClosePopover = () => {
+ this.props.onClosePopover()
+ }
+
+ render() {
+ const {
+ expiresAtValue,
+ intl,
+ isXS,
+ } = this.props
+
+ const listItems = [
+ {
+ hideArrow: true,
+ title: 'None',
+ onClick: () => this.handleOnSetStatusExpiration(null),
+ isActive: !expiresAtValue,
+ },
+ {
+ hideArrow: true,
+ title: intl.formatMessage(messages.minutes, { number: 5 }),
+ onClick: () => this.handleOnSetStatusExpiration(EXPIRATION_OPTION_5_MINUTES),
+ isActive: expiresAtValue === EXPIRATION_OPTION_5_MINUTES,
+ },
+ {
+ hideArrow: true,
+ title: intl.formatMessage(messages.minutes, { number: 60 }),
+ onClick: () => this.handleOnSetStatusExpiration(EXPIRATION_OPTION_60_MINUTES),
+ isActive: expiresAtValue === EXPIRATION_OPTION_60_MINUTES,
+ },
+ {
+ hideArrow: true,
+ title: '6 hours',
+ title: intl.formatMessage(messages.hours, { number: 6 }),
+ onClick: () => this.handleOnSetStatusExpiration(EXPIRATION_OPTION_6_HOURS),
+ isActive: expiresAtValue === EXPIRATION_OPTION_6_HOURS,
+ },
+ {
+ hideArrow: true,
+ title: intl.formatMessage(messages.hours, { number: 24 }),
+ onClick: () => this.handleOnSetStatusExpiration(EXPIRATION_OPTION_24_HOURS),
+ isActive: expiresAtValue === EXPIRATION_OPTION_24_HOURS,
+ },
+ {
+ hideArrow: true,
+ title: '3 days',
+ title: intl.formatMessage(messages.days, { number: 3 }),
+ onClick: () => this.handleOnSetStatusExpiration(EXPIRATION_OPTION_3_DAYS),
+ isActive: expiresAtValue === EXPIRATION_OPTION_3_DAYS,
+ },
+ {
+ hideArrow: true,
+ title: intl.formatMessage(messages.days, { number: 7 }),
+ onClick: () => this.handleOnSetStatusExpiration(EXPIRATION_OPTION_7_DAYS),
+ isActive: expiresAtValue === EXPIRATION_OPTION_7_DAYS,
+ },
+ ]
+
+ if (expiresAtValue) {
+ listItems.unshift({
+ hideArrow: true,
+ title: 'Remove expiration',
+ onClick: () => this.handleOnSetStatusExpiration(null),
+ },)
+ }
+
+ return (
+
+ This chats delete after:
+
+
+ )
+ }
+
+}
+
+const messages = defineMessages({
+ minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' },
+ hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' },
+ days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' },
+})
+
+const mapStateToProps = (state) => ({
+ expiresAtValue: state.getIn(['compose', 'expires_at']),
+})
+
+const mapDispatchToProps = (dispatch) => ({
+ onChangeExpiresAt(expiresAt) {
+ dispatch(changeExpiresAt(expiresAt))
+ },
+ onClosePopover() {
+ dispatch(closePopover())
+ },
+})
+
+ChatConversationExpirationOptionsPopover.defaultProps = {
+ expiresAtValue: PropTypes.string.isRequired,
+ intl: PropTypes.object.isRequired,
+ isXS: PropTypes.bool,
+ onChangeExpiresAt: PropTypes.func.isRequired,
+}
+
+export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ChatConversationExpirationOptionsPopover))
\ No newline at end of file
diff --git a/app/javascript/gabsocial/components/popover/chat_conversation_options_popover.js b/app/javascript/gabsocial/components/popover/chat_conversation_options_popover.js
index a9655d34..70fdcf19 100644
--- a/app/javascript/gabsocial/components/popover/chat_conversation_options_popover.js
+++ b/app/javascript/gabsocial/components/popover/chat_conversation_options_popover.js
@@ -5,14 +5,8 @@ import ImmutablePureComponent from 'react-immutable-pure-component'
import { connect } from 'react-redux'
import { closePopover } from '../../actions/popover'
import { openModal } from '../../actions/modal'
-import {
- isChatMessengerBlocked,
- isChatMessengerMuted,
- blockChatMessenger,
- unblockChatMessenger,
- muteChatMessenger,
- unmuteChatMessenger,
-} from '../../actions/chat_conversation_accounts'
+import { hideChatConversation } from '../../actions/chat_conversations'
+import { purgeChatMessages } from '../../actions/chat_messages'
import { MODAL_PRO_UPGRADE } from '../../constants'
import { me } from '../../initial_state'
import { makeGetChatConversation } from '../../selectors'
@@ -27,21 +21,6 @@ class ChatConversationOptionsPopover extends ImmutablePureComponent {
this.handleOnClosePopover()
}
- handleOnBlock = () => {
- this.props.onBlock()
- this.handleOnClosePopover()
- }
-
- handleOnUnblock = () => {
- this.props.onUnblock()
- this.handleOnClosePopover()
- }
-
- handleOnMute = () => {
- this.props.onMute()
- this.handleOnClosePopover()
- }
-
handleOnUnmute = () => {
this.props.onUnute()
this.handleOnClosePopover()
@@ -51,7 +30,7 @@ class ChatConversationOptionsPopover extends ImmutablePureComponent {
if (!this.props.isPro) {
this.props.openProUpgradeModal()
} else {
- this.props.onPurge()
+ this.props.onPurge(this.props.chatConversationId)
}
this.handleOnClosePopover()
@@ -68,18 +47,6 @@ class ChatConversationOptionsPopover extends ImmutablePureComponent {
} = this.props
const items = [
- {
- hideArrow: true,
- title: 'Block Messenger',
- subtitle: 'The messenger will not be able to message you.',
- onClick: () => this.handleOnBlock(),
- },
- {
- hideArrow: true,
- title: 'Mute Messenger',
- subtitle: 'You will not be notified of new messsages',
- onClick: () => this.handleOnMute(),
- },
{
hideArrow: true,
title: 'Hide Conversation',
@@ -123,6 +90,12 @@ const mapDispatchToProps = (dispatch) => ({
onSetCommentSortingSetting(type) {
dispatch(closePopover())
},
+ onPurge(chatConversationId) {
+ dispatch(purgeChatMessages(chatConversationId))
+ },
+ onHide(chatConversationId) {
+ dispatch(hideChatConversation(chatConversationId))
+ },
onClosePopover: () => dispatch(closePopover()),
})
diff --git a/app/javascript/gabsocial/components/popover/chat_message_delete_popover.js b/app/javascript/gabsocial/components/popover/chat_message_delete_popover.js
deleted file mode 100644
index a822df01..00000000
--- a/app/javascript/gabsocial/components/popover/chat_message_delete_popover.js
+++ /dev/null
@@ -1,58 +0,0 @@
-import React from 'react'
-import PropTypes from 'prop-types'
-import { connect } from 'react-redux'
-import { closePopover } from '../../actions/popover'
-import { deleteChatMessage } from '../../actions/chat_messages'
-import PopoverLayout from './popover_layout'
-import Button from '../button'
-import Text from '../text'
-
-class ChatMessageDeletePopover extends React.PureComponent {
-
- handleOnClick = () => {
- this.props.onDeleteChatMessage(this.props.chatMessageId)
- }
-
- handleOnClosePopover = () => {
- this.props.onClosePopover()
- }
-
- render() {
- const { isXS } = this.props
-
- return (
-
-
-
- )
- }
-}
-
-const mapDispatchToProps = (dispatch) => ({
- onDeleteChatMessage(chatMessageId) {
- dispatch(deleteChatMessage(chatMessageId))
- dispatch(closePopover())
- },
- onClosePopover() {
- dispatch(closePopover())
- },
-})
-
-ChatMessageDeletePopover.propTypes = {
- isXS: PropTypes.bool,
- chatMessageId: PropTypes.string.isRequired,
- onDeleteChatMessage: PropTypes.func.isRequired,
-}
-
-export default connect(null, mapDispatchToProps)(ChatMessageDeletePopover)
\ No newline at end of file
diff --git a/app/javascript/gabsocial/components/popover/chat_message_options_popover.js b/app/javascript/gabsocial/components/popover/chat_message_options_popover.js
new file mode 100644
index 00000000..9ce25bef
--- /dev/null
+++ b/app/javascript/gabsocial/components/popover/chat_message_options_popover.js
@@ -0,0 +1,139 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import { connect } from 'react-redux'
+import { closePopover } from '../../actions/popover'
+import { deleteChatMessage } from '../../actions/chat_messages'
+import {
+ isChatMessengerBlocked,
+ isChatMessengerMuted,
+ blockChatMessenger,
+ unblockChatMessenger,
+ muteChatMessenger,
+ unmuteChatMessenger,
+ reportChatMessage,
+} from '../../actions/chat_conversation_accounts'
+import { makeGetChatMessage } from '../../selectors'
+import { me } from '../../initial_state'
+import PopoverLayout from './popover_layout'
+import Button from '../button'
+import List from '../list'
+import Text from '../text'
+
+class ChatMessageOptionsPopover extends React.PureComponent {
+
+ handleOnDelete = () => {
+ this.props.onDeleteChatMessage(this.props.chatMessageId)
+ }
+
+ handleOnReport = () => {
+ this.props.onReportChatMessage(this.props.chatMessageId)
+ }
+
+ handleOnBlock = () => {
+ if (this.props.isBlocked) {
+ this.props.unblockChatMessenger(this.props.fromAccountId)
+ } else {
+ this.props.blockChatMessenger(this.props.fromAccountId)
+ }
+ }
+
+ handleOnMute = () => {
+ if (this.props.isMuted) {
+ this.props.unmuteChatMessenger(this.props.fromAccountId)
+ } else {
+ this.props.muteChatMessenger(this.props.fromAccountId)
+ }
+ }
+
+ handleOnClosePopover = () => {
+ this.props.onClosePopover()
+ }
+
+ render() {
+ const {
+ isXS,
+ isMine,
+ isMuted,
+ isBlocked,
+ } = this.props
+
+ const items = isMine ? [
+ {
+ hideArrow: true,
+ title: 'Delete Message',
+ onClick: () => this.handleOnDelete(),
+ }
+ ] : [
+ {
+ hideArrow: true,
+ title: 'Report Messenger',
+ onClick: () => this.handleOnReport(),
+ },
+ {},
+ {
+ hideArrow: true,
+ title: isBlocked ? 'Unblock Messenger' : 'Block Messenger',
+ subtitle: isBlocked ? '' : 'The messenger will not be able to message you.',
+ onClick: () => this.handleOnBlock(),
+ },
+ {
+ hideArrow: true,
+ title: isMuted ? 'Unmute Messenger' : 'Mute Messenger',
+ subtitle: isMuted ? '' : 'You will not be notified of new messsages',
+ onClick: () => this.handleOnMute(),
+ },
+ ]
+
+ return (
+
+
+
+ )
+ }
+}
+
+const mapStateToProps = (state, { chatMessageId }) => ({
+ isMine: state.getIn(['chat_messages', chatMessageId, 'from_account_id']) === me,
+ fromAccountId: state.getIn(['chat_messages', chatMessageId, 'from_account_id']),
+ isBlocked: state.getIn(['chat_messages', chatMessageId, 'from_account_id']),
+ isMuted: state.getIn(['chat_messages', chatMessageId, 'from_account_id']),
+})
+
+const mapDispatchToProps = (dispatch) => ({
+ onDeleteChatMessage(chatMessageId) {
+ dispatch(deleteChatMessage(chatMessageId))
+ dispatch(closePopover())
+ },
+ onBlock(accountId) {
+ dispatch(blockChatMessenger(accountId))
+ },
+ onUnblock(accountId) {
+ dispatch(unblockChatMessenger(accountId))
+ },
+ onMute(accountId) {
+ dispatch(muteChatMessenger(accountId))
+ },
+ onUnmute(accountId) {
+ dispatch(unmuteChatMessenger(accountId))
+ },
+ onReportChatMessage(chatMessageId) {
+ dispatch(reportChatMessage(chatMessageId))
+ },
+ onClosePopover() {
+ dispatch(closePopover())
+ },
+})
+
+ChatMessageOptionsPopover.propTypes = {
+ isXS: PropTypes.bool,
+ chatMessageId: PropTypes.string.isRequired,
+ isBlocked: PropTypes.bool.isRequired,
+ isMuted: PropTypes.bool.isRequired,
+ onDeleteChatMessage: PropTypes.func.isRequired,
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(ChatMessageOptionsPopover)
\ No newline at end of file
diff --git a/app/javascript/gabsocial/components/popover/compose_post_destination_popover.js b/app/javascript/gabsocial/components/popover/compose_post_destination_popover.js
new file mode 100644
index 00000000..998898ca
--- /dev/null
+++ b/app/javascript/gabsocial/components/popover/compose_post_destination_popover.js
@@ -0,0 +1,61 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import { connect } from 'react-redux'
+import { closePopover } from '../../actions/popover'
+import PopoverLayout from './popover_layout'
+import List from '../list'
+import Text from '../text'
+
+class ComposePostDesinationPopover extends React.PureComponent {
+
+ handleOnClosePopover = () => {
+ this.props.onClosePopover()
+ }
+
+ render() {
+ const {
+ isXS,
+ } = this.props
+
+ // TIMELINE
+ // GROUP - MY GROUPS
+
+ const items = [
+ {
+ hideArrow: true,
+ title: 'Timeline',
+ onClick: () => this.handleOnDelete(),
+ },
+ {
+ title: 'Group',
+ onClick: () => this.handleOnReport(),
+ },
+ ]
+
+ return (
+
+ Post to:
+
+
+ )
+ }
+}
+
+const mapStateToProps = (state) => ({
+ //
+})
+
+const mapDispatchToProps = (dispatch) => ({
+ onClosePopover: () => dispatch(closePopover()),
+})
+
+ComposePostDesinationPopover.propTypes = {
+ isXS: PropTypes.bool,
+ onClosePopover: PropTypes.func.isRequired,
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(ComposePostDesinationPopover)
\ No newline at end of file
diff --git a/app/javascript/gabsocial/components/popover/popover_root.js b/app/javascript/gabsocial/components/popover/popover_root.js
index 59d4f201..a6c6b0ca 100644
--- a/app/javascript/gabsocial/components/popover/popover_root.js
+++ b/app/javascript/gabsocial/components/popover/popover_root.js
@@ -1,8 +1,9 @@
import {
BREAKPOINT_EXTRA_SMALL,
POPOVER_CHAT_CONVERSATION_OPTIONS,
- POPOVER_CHAT_MESSAGE_DELETE,
+ POPOVER_CHAT_MESSAGE_OPTIONS,
POPOVER_COMMENT_SORTING_OPTIONS,
+ POPOVER_COMPOSE_POST_DESTINATION,
POPOVER_DATE_PICKER,
POPOVER_EMOJI_PICKER,
POPOVER_GROUP_LIST_SORT_OPTIONS,
@@ -23,8 +24,9 @@ import {
} from '../../constants'
import {
ChatConversationOptionsPopover,
- ChatMessageDeletePopover,
+ ChatMessageOptionsPopover,
CommentSortingOptionsPopover,
+ ComposePostDesinationPopover,
DatePickerPopover,
EmojiPickerPopover,
GroupListSortOptionsPopover,
@@ -59,8 +61,9 @@ const initialState = getWindowDimension()
const POPOVER_COMPONENTS = {
[POPOVER_CHAT_CONVERSATION_OPTIONS]: ChatConversationOptionsPopover,
- [POPOVER_CHAT_MESSAGE_DELETE]: ChatMessageDeletePopover,
+ [POPOVER_CHAT_MESSAGE_OPTIONS]: ChatMessageOptionsPopover,
[POPOVER_COMMENT_SORTING_OPTIONS]: CommentSortingOptionsPopover,
+ [POPOVER_COMPOSE_POST_DESTINATION]: ComposePostDesinationPopover,
[POPOVER_DATE_PICKER]: DatePickerPopover,
[POPOVER_EMOJI_PICKER]: EmojiPickerPopover,
[POPOVER_GROUP_LIST_SORT_OPTIONS]: GroupListSortOptionsPopover,
diff --git a/app/javascript/gabsocial/components/popover/status_expiration_options_popover.js b/app/javascript/gabsocial/components/popover/status_expiration_options_popover.js
index ac0f9c46..61108332 100644
--- a/app/javascript/gabsocial/components/popover/status_expiration_options_popover.js
+++ b/app/javascript/gabsocial/components/popover/status_expiration_options_popover.js
@@ -5,15 +5,16 @@ import { defineMessages, injectIntl } from 'react-intl'
import { closePopover } from '../../actions/popover'
import { changeExpiresAt } from '../../actions/compose'
import {
- STATUS_EXPIRATION_OPTION_5_MINUTES,
- STATUS_EXPIRATION_OPTION_60_MINUTES,
- STATUS_EXPIRATION_OPTION_6_HOURS,
- STATUS_EXPIRATION_OPTION_24_HOURS,
- STATUS_EXPIRATION_OPTION_3_DAYS,
- STATUS_EXPIRATION_OPTION_7_DAYS,
+ EXPIRATION_OPTION_5_MINUTES,
+ EXPIRATION_OPTION_60_MINUTES,
+ EXPIRATION_OPTION_6_HOURS,
+ EXPIRATION_OPTION_24_HOURS,
+ EXPIRATION_OPTION_3_DAYS,
+ EXPIRATION_OPTION_7_DAYS,
} from '../../constants'
import PopoverLayout from './popover_layout'
import List from '../list'
+import Text from '../text'
class StatusExpirationOptionsPopover extends React.PureComponent {
@@ -34,43 +35,49 @@ class StatusExpirationOptionsPopover extends React.PureComponent {
} = this.props
const listItems = [
+ {
+ hideArrow: true,
+ title: 'None',
+ onClick: () => this.handleOnSetStatusExpiration(null),
+ isActive: !expiresAtValue,
+ },
{
hideArrow: true,
title: intl.formatMessage(messages.minutes, { number: 5 }),
- onClick: () => this.handleOnSetStatusExpiration(STATUS_EXPIRATION_OPTION_5_MINUTES),
- isActive: expiresAtValue === STATUS_EXPIRATION_OPTION_5_MINUTES,
+ onClick: () => this.handleOnSetStatusExpiration(EXPIRATION_OPTION_5_MINUTES),
+ isActive: expiresAtValue === EXPIRATION_OPTION_5_MINUTES,
},
{
hideArrow: true,
title: intl.formatMessage(messages.minutes, { number: 60 }),
- onClick: () => this.handleOnSetStatusExpiration(STATUS_EXPIRATION_OPTION_60_MINUTES),
- isActive: expiresAtValue === STATUS_EXPIRATION_OPTION_60_MINUTES,
+ onClick: () => this.handleOnSetStatusExpiration(EXPIRATION_OPTION_60_MINUTES),
+ isActive: expiresAtValue === EXPIRATION_OPTION_60_MINUTES,
},
{
hideArrow: true,
title: '6 hours',
title: intl.formatMessage(messages.hours, { number: 6 }),
- onClick: () => this.handleOnSetStatusExpiration(STATUS_EXPIRATION_OPTION_6_HOURS),
- isActive: expiresAtValue === STATUS_EXPIRATION_OPTION_6_HOURS,
+ onClick: () => this.handleOnSetStatusExpiration(EXPIRATION_OPTION_6_HOURS),
+ isActive: expiresAtValue === EXPIRATION_OPTION_6_HOURS,
},
{
hideArrow: true,
title: intl.formatMessage(messages.hours, { number: 24 }),
- onClick: () => this.handleOnSetStatusExpiration(STATUS_EXPIRATION_OPTION_24_HOURS),
- isActive: expiresAtValue === STATUS_EXPIRATION_OPTION_24_HOURS,
+ onClick: () => this.handleOnSetStatusExpiration(EXPIRATION_OPTION_24_HOURS),
+ isActive: expiresAtValue === EXPIRATION_OPTION_24_HOURS,
},
{
hideArrow: true,
title: '3 days',
title: intl.formatMessage(messages.days, { number: 3 }),
- onClick: () => this.handleOnSetStatusExpiration(STATUS_EXPIRATION_OPTION_3_DAYS),
- isActive: expiresAtValue === STATUS_EXPIRATION_OPTION_3_DAYS,
+ onClick: () => this.handleOnSetStatusExpiration(EXPIRATION_OPTION_3_DAYS),
+ isActive: expiresAtValue === EXPIRATION_OPTION_3_DAYS,
},
{
hideArrow: true,
title: intl.formatMessage(messages.days, { number: 7 }),
- onClick: () => this.handleOnSetStatusExpiration(STATUS_EXPIRATION_OPTION_7_DAYS),
- isActive: expiresAtValue === STATUS_EXPIRATION_OPTION_7_DAYS,
+ onClick: () => this.handleOnSetStatusExpiration(EXPIRATION_OPTION_7_DAYS),
+ isActive: expiresAtValue === EXPIRATION_OPTION_7_DAYS,
},
]
@@ -88,8 +95,9 @@ class StatusExpirationOptionsPopover extends React.PureComponent {
isXS={isXS}
onClose={this.handleOnClosePopover}
>
+
This gab deletes after:
diff --git a/app/javascript/gabsocial/components/popover/status_share_popover.js b/app/javascript/gabsocial/components/popover/status_share_popover.js
index ad520c82..4ed4634c 100644
--- a/app/javascript/gabsocial/components/popover/status_share_popover.js
+++ b/app/javascript/gabsocial/components/popover/status_share_popover.js
@@ -5,6 +5,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'
import { connect } from 'react-redux'
import { defineMessages, injectIntl } from 'react-intl'
import { openModal } from '../../actions/modal'
+import { showToast } from '../../actions/toasts'
import { closePopover } from '../../actions/popover'
import PopoverLayout from './popover_layout'
import Button from '../button'
@@ -31,6 +32,7 @@ class StatusSharePopover extends ImmutablePureComponent {
}
document.body.removeChild(textarea)
+ this.props.onShowCopyToast()
this.handleClosePopover()
}
@@ -157,6 +159,9 @@ const messages = defineMessages({
const mapDispatchToProps = (dispatch) => ({
onClosePopover: () => dispatch(closePopover()),
+ onShowCopyToast() {
+ dispatch(showToast())
+ },
})
StatusSharePopover.propTypes = {
diff --git a/app/javascript/gabsocial/components/popover/status_visibility_popover.js b/app/javascript/gabsocial/components/popover/status_visibility_popover.js
index cd393808..360abc9a 100644
--- a/app/javascript/gabsocial/components/popover/status_visibility_popover.js
+++ b/app/javascript/gabsocial/components/popover/status_visibility_popover.js
@@ -49,6 +49,7 @@ class StatusVisibilityDropdown extends React.PureComponent {
isXS={isXS}
onClose={this.handleOnClosePopover}
>
+
Status Visibility:
{
options.map((option, i) => {
diff --git a/app/javascript/gabsocial/components/rich_text_editor_bar.js b/app/javascript/gabsocial/components/rich_text_editor_bar.js
index 1a99dfce..ab46b2d8 100644
--- a/app/javascript/gabsocial/components/rich_text_editor_bar.js
+++ b/app/javascript/gabsocial/components/rich_text_editor_bar.js
@@ -148,7 +148,7 @@ class StyleButton extends React.PureComponent {
px10: 1,
mr5: 1,
noSelect: 1,
- bgSecondaryDark_onHover: 1,
+ bgSubtle_onHover: 1,
bgBrandLight: active,
bgTransparent: 1,
radiusSmall: 1,
@@ -162,7 +162,7 @@ class StyleButton extends React.PureComponent {
onMouseDown={this.handleOnClick}
title={label}
>
-
+
)
}
diff --git a/app/javascript/gabsocial/components/sidebar/deck_sidebar.js b/app/javascript/gabsocial/components/sidebar/deck_sidebar.js
index dd7ae2d5..456d2140 100644
--- a/app/javascript/gabsocial/components/sidebar/deck_sidebar.js
+++ b/app/javascript/gabsocial/components/sidebar/deck_sidebar.js
@@ -43,12 +43,16 @@ class DeckSidebar extends ImmutablePureComponent {
this.props.onOpenComposeModal()
}
+ scrollToItem = () => {
+
+ }
+
setAvatarNode = (c) => {
this.avatarNode = c
}
render() {
- const { account, logoDisabled } = this.props
+ const { account, gabDeckOrder, logoDisabled } = this.props
const isPro = !!account ? account.get('is_pro') : false
@@ -83,6 +87,22 @@ class DeckSidebar extends ImmutablePureComponent {
+
+ {
+ !!gabDeckOrder && gabDeckOrder.map((item, i) => (
+
+ ))
+ }
+
+
{ isPro &&
}
@@ -119,6 +139,7 @@ const mapStateToProps = (state) => ({
account: makeGetAccount()(state, me),
theme: state.getIn(['settings', 'displayOptions', 'theme'], DEFAULT_THEME),
logoDisabled: state.getIn(['settings', 'displayOptions', 'logoDisabled'], false),
+ gabDeckOrder: state.getIn(['settings', 'gabDeckOrder']),
})
const mapDispatchToProps = (dispatch) => ({
diff --git a/app/javascript/gabsocial/components/timeline_compose_block.js b/app/javascript/gabsocial/components/timeline_compose_block.js
index 1911821a..a265d77f 100644
--- a/app/javascript/gabsocial/components/timeline_compose_block.js
+++ b/app/javascript/gabsocial/components/timeline_compose_block.js
@@ -10,12 +10,14 @@ import ComposeFormContainer from '../features/compose/containers/compose_form_co
import ResponsiveClassesComponent from '../features/ui/util/responsive_classes_component'
import Responsive from '../features/ui/util/responsive_component'
import Avatar from './avatar'
-import Heading from './heading'
+import Button from './button'
+import Text from './text'
class TimelineComposeBlock extends ImmutablePureComponent {
render() {
const {
+ formLocation,
account,
size,
intl,
@@ -27,7 +29,7 @@ class TimelineComposeBlock extends ImmutablePureComponent {
return (
)
@@ -39,17 +41,7 @@ class TimelineComposeBlock extends ImmutablePureComponent {
classNames={[_s.d, _s.boxShadowBlock, _s.bgPrimary, _s.overflowHidden, _s.radiusSmall].join(' ')}
classNamesXS={[_s.d, _s.boxShadowBlock, _s.bgPrimary, _s.overflowHidden].join(' ')}
>
-
-
-
-
- {intl.formatMessage(messages.createPost)}
-
-
-
-
+
)
@@ -70,10 +62,12 @@ TimelineComposeBlock.propTypes = {
account: ImmutablePropTypes.map.isRequired,
size: PropTypes.number,
isModal: PropTypes.bool,
+ formLocation: PropTypes.string,
}
TimelineComposeBlock.defaultProps = {
size: 32,
+ formLocation: 'timeline',
}
export default injectIntl(connect(mapStateToProps)(TimelineComposeBlock))
\ No newline at end of file
diff --git a/app/javascript/gabsocial/components/video.js b/app/javascript/gabsocial/components/video.js
index f7e666b5..49db90cd 100644
--- a/app/javascript/gabsocial/components/video.js
+++ b/app/javascript/gabsocial/components/video.js
@@ -344,7 +344,7 @@ class Video extends ImmutablePureComponent {
this.video.play()
}
setTimeout(() => { // : hack :
- this.video.requestPictureInPicture()
+ this.video.requestPictureInPicture()
}, 500)
} else {
document.exitPictureInPicture()
diff --git a/app/javascript/gabsocial/constants.js b/app/javascript/gabsocial/constants.js
index 0449f04a..31c593a7 100644
--- a/app/javascript/gabsocial/constants.js
+++ b/app/javascript/gabsocial/constants.js
@@ -10,7 +10,6 @@ export const BREAKPOINT_LARGE = 1280
export const BREAKPOINT_MEDIUM = 1160
export const BREAKPOINT_SMALL = 1080
export const BREAKPOINT_EXTRA_SMALL = 992
-export const BREAKPOINT_EXTRA_EXTRA_SMALL = 767
export const MOUSE_IDLE_DELAY = 300
@@ -26,8 +25,9 @@ export const URL_GAB_PRO = 'https://pro.gab.com'
export const PLACEHOLDER_MISSING_HEADER_SRC = '/original/missing.png'
export const POPOVER_CHAT_CONVERSATION_OPTIONS = 'CHAT_CONVERSATION_OPTIONS'
-export const POPOVER_CHAT_MESSAGE_DELETE = 'CHAT_MESSAGE_DELETE'
+export const POPOVER_CHAT_MESSAGE_OPTIONS = 'CHAT_MESSAGE_OPTIONS'
export const POPOVER_COMMENT_SORTING_OPTIONS = 'COMMENT_SORTING_OPTIONS'
+export const POPOVER_COMPOSE_POST_DESTINATION = 'COMPOSE_POST_DESTINATION'
export const POPOVER_DATE_PICKER = 'DATE_PICKER'
export const POPOVER_EMOJI_PICKER = 'EMOJI_PICKER'
export const POPOVER_GROUP_LIST_SORT_OPTIONS = 'GROUP_LIST_SORT_OPTIONS'
@@ -54,6 +54,7 @@ export const MODAL_COMMUNITY_TIMELINE_SETTINGS = 'COMMUNITY_TIMELINE_SETTINGS'
export const MODAL_COMPOSE = 'COMPOSE'
export const MODAL_CONFIRM = 'CONFIRM'
export const MODAL_DECK_COLUMN_ADD = 'DECK_COLUMN_ADD'
+export const MODAL_DECK_COLUMN_ADD_OPTIONS = 'DECK_COLUMN_ADD_OPTIONS'
export const MODAL_DISPLAY_OPTIONS = 'DISPLAY_OPTIONS'
export const MODAL_EDIT_PROFILE = 'EDIT_PROFILE'
export const MODAL_EDIT_SHORTCUTS = 'EDIT_SHORTCUTS'
@@ -130,12 +131,12 @@ export const GAB_COM_INTRODUCE_YOURSELF_GROUP_ID = '12'
export const MIN_ACCOUNT_CREATED_AT_ONBOARDING = 1594789200000 // 2020-07-15
-export const STATUS_EXPIRATION_OPTION_5_MINUTES = '5-minutes'
-export const STATUS_EXPIRATION_OPTION_60_MINUTES = '60-minutes'
-export const STATUS_EXPIRATION_OPTION_6_HOURS = '6-hours'
-export const STATUS_EXPIRATION_OPTION_24_HOURS = '24-hours'
-export const STATUS_EXPIRATION_OPTION_3_DAYS = '3-days'
-export const STATUS_EXPIRATION_OPTION_7_DAYS = '7-days'
+export const EXPIRATION_OPTION_5_MINUTES = 'five_minutes'
+export const EXPIRATION_OPTION_60_MINUTES = 'one_hour'
+export const EXPIRATION_OPTION_6_HOURS = 'six_hours'
+export const EXPIRATION_OPTION_24_HOURS = 'one_day'
+export const EXPIRATION_OPTION_3_DAYS = 'three_days'
+export const EXPIRATION_OPTION_7_DAYS = 'one_week'
export const GROUP_TIMELINE_SORTING_TYPE_HOT = 'hot'
export const GROUP_TIMELINE_SORTING_TYPE_NEWEST = 'newest'
diff --git a/app/javascript/gabsocial/features/bookmark_collections.js b/app/javascript/gabsocial/features/bookmark_collections.js
new file mode 100644
index 00000000..fc9c47ec
--- /dev/null
+++ b/app/javascript/gabsocial/features/bookmark_collections.js
@@ -0,0 +1,64 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import { connect } from 'react-redux'
+import ImmutablePureComponent from 'react-immutable-pure-component'
+import ImmutablePropTypes from 'react-immutable-proptypes'
+import { fetchBookmarkCollections } from '../actions/bookmarks'
+import ColumnIndicator from '../components/column_indicator'
+import List from '../components/list'
+
+class BookmarkCollections extends ImmutablePureComponent {
+
+ componentDidMount() {
+ this.props.onFetchBookmarkCollections()
+ }
+
+ render() {
+ const {
+ isLoading,
+ isError,
+ bookmarkCollections,
+ } = this.props
+
+ if (isError) {
+ return
+ }
+
+ const listItems = shortcuts.map((s) => ({
+ to: s.get('to'),
+ title: s.get('title'),
+ image: s.get('image'),
+ }))
+
+ return (
+
+ )
+ }
+
+}
+
+const mapStateToProps = (state) => ({
+ isError: state.getIn(['bookmark_collections', 'isError']),
+ isLoading: state.getIn(['bookmark_collections', 'isLoading']),
+ shortcuts: state.getIn(['bookmark_collections', 'items']),
+})
+
+const mapDispatchToProps = (dispatch) => ({
+ onFetchBookmarkCollections() {
+ dispatch(fetchBookmarkCollections())
+ },
+})
+
+BookmarkCollections.propTypes = {
+ isLoading: PropTypes.bool.isRequired,
+ isError: PropTypes.bool.isRequired,
+ onFetchBookmarkCollections: PropTypes.func.isRequired,
+ bookmarkCollections: ImmutablePropTypes.list,
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(BookmarkCollections)
\ No newline at end of file
diff --git a/app/javascript/gabsocial/features/compose/components/compose_destination_header.js b/app/javascript/gabsocial/features/compose/components/compose_destination_header.js
index b57c2ebf..8ca6879e 100644
--- a/app/javascript/gabsocial/features/compose/components/compose_destination_header.js
+++ b/app/javascript/gabsocial/features/compose/components/compose_destination_header.js
@@ -1,75 +1,94 @@
import React from 'react'
import PropTypes from 'prop-types'
-import { defineMessages, injectIntl } from 'react-intl'
+import { connect } from 'react-redux'
import ImmutablePureComponent from 'react-immutable-pure-component'
import ImmutablePropTypes from 'react-immutable-proptypes'
-import { length } from 'stringz'
-import { isMobile } from '../../../utils/is_mobile'
-import { countableText } from '../../ui/util/counter'
import {
CX,
- MAX_POST_CHARACTER_COUNT,
- ALLOWED_AROUND_SHORT_CODE,
- BREAKPOINT_EXTRA_SMALL,
- BREAKPOINT_EXTRA_EXTRA_SMALL,
- BREAKPOINT_MEDIUM,
+ MODAL_COMPOSE,
+ POPOVER_COMPOSE_POST_DESTINATION,
} from '../../../constants'
-import AutosuggestTextbox from '../../../components/autosuggest_textbox'
-import Responsive from '../../ui/util/responsive_component'
-import ResponsiveClassesComponent from '../../ui/util/responsive_classes_component'
+import { openModal } from '../../../actions/modal'
+import { openPopover } from '../../../actions/popover'
import Avatar from '../../../components/avatar'
import Button from '../../../components/button'
-import EmojiPickerButton from './emoji_picker_button'
-import PollButton from './poll_button'
-import PollForm from './poll_form'
-import SchedulePostButton from './schedule_post_button'
-import SpoilerButton from './spoiler_button'
-import ExpiresPostButton from './expires_post_button'
-import RichTextEditorButton from './rich_text_editor_button'
-import StatusContainer from '../../../containers/status_container'
-import StatusVisibilityButton from './status_visibility_button'
-import UploadButton from './media_upload_button'
-import UploadForm from './upload_form'
-import Input from '../../../components/input'
-import Text from '../../../components/text'
import Icon from '../../../components/icon'
-import ComposeExtraButtonList from './compose_extra_button_list'
+import Text from '../../../components/text'
class ComposeDestinationHeader extends ImmutablePureComponent {
handleOnClick = () => {
+ this.props.onOpenPopover(this.desinationBtn)
+ }
+ handleOnExpand = () => {
+ this.props.onOpenModal()
+ }
+
+ setDestinationBtn = (c) => {
+ this.desinationBtn = c
}
render() {
- const { account } = this.props
+ const { account, isModal } = this.props
const title = 'Post to timeline'
return (
-
-
-
-
+
+
+
+
+
+
+ {
+ !isModal &&
+
+ }
)
}
}
+const mapDispatchToProps = (dispatch) => ({
+ onOpenModal() {
+ dispatch(openModal(MODAL_COMPOSE))
+ },
+ onOpenPopover(targetRef) {
+ dispatch(openPopover(POPOVER_COMPOSE_POST_DESTINATION, {
+ targetRef,
+ position: 'bottom',
+ }))
+ },
+})
+
ComposeDestinationHeader.propTypes = {
account: ImmutablePropTypes.map,
+ isModal: PropTypes.bool,
+ onOpenModal: PropTypes.func.isRequired,
+ onOpenPopover: PropTypes.func.isRequired,
}
-export default ComposeDestinationHeader
\ No newline at end of file
+export default connect(null, mapDispatchToProps)(ComposeDestinationHeader)
\ No newline at end of file
diff --git a/app/javascript/gabsocial/features/compose/components/compose_extra_button.js b/app/javascript/gabsocial/features/compose/components/compose_extra_button.js
index f2b09ce8..ecb716df 100644
--- a/app/javascript/gabsocial/features/compose/components/compose_extra_button.js
+++ b/app/javascript/gabsocial/features/compose/components/compose_extra_button.js
@@ -22,14 +22,14 @@ class ComposeExtraButton extends React.PureComponent {
const containerClasses = CX({
d: 1,
- mr5: 1,
jcCenter: 1,
h40PX: 1,
+ mr5: 1,
})
const btnClasses = CX({
d: 1,
- circle: 1,
+ circle: small,
noUnderline: 1,
font: 1,
cursorPointer: 1,
@@ -37,21 +37,25 @@ class ComposeExtraButton extends React.PureComponent {
outlineNone: 1,
bgTransparent: 1,
flexRow: 1,
+ aiCenter: 1,
+ // jcCenter: !small,
bgSubtle_onHover: !active,
bgBrandLight: active,
py10: 1,
- px10: 1,
+ px10: small,
+ radiusSmall: !small,
})
- const iconClasses = CX(iconClassName, {
+ const iconClasses = CX(active ? null : iconClassName, {
cSecondary: !active,
cWhite: active,
- mr10: 1,
+ mr10: !small,
py2: small,
- ml10: small,
+ ml10: !small,
+ px2: small,
})
- const iconSize = !small ? '18px' : '16px'
+ const iconSize = '16px'
const textColor = !active ? 'primary' : 'white'
return (
@@ -65,13 +69,13 @@ class ComposeExtraButton extends React.PureComponent {
backgroundColor='none'
iconClassName={iconClasses}
icon={icon}
- iconSize={iconSize}
+ iconSize='16px'
buttonRef={!children ? buttonRef : undefined}
>
{ children }
{
!small &&
-
+
{title}
}
diff --git a/app/javascript/gabsocial/features/compose/components/compose_extra_button_list.js b/app/javascript/gabsocial/features/compose/components/compose_extra_button_list.js
index c20de238..83e16957 100644
--- a/app/javascript/gabsocial/features/compose/components/compose_extra_button_list.js
+++ b/app/javascript/gabsocial/features/compose/components/compose_extra_button_list.js
@@ -6,6 +6,7 @@ import {
} from '../../../constants'
import Responsive from '../../ui/util/responsive_component'
import ResponsiveClassesComponent from '../../ui/util/responsive_classes_component'
+import Text from '../../../components/text'
import EmojiPickerButton from './emoji_picker_button'
import PollButton from './poll_button'
import SchedulePostButton from './schedule_post_button'
@@ -22,6 +23,7 @@ class ComposeExtraButtonList extends React.PureComponent {
state = {
height: initialState.height,
+ width: initialState.width,
}
componentDidMount() {
@@ -31,9 +33,9 @@ class ComposeExtraButtonList extends React.PureComponent {
}
handleResize = () => {
- const { height } = getWindowDimension()
+ const { height, width } = getWindowDimension()
- this.setState({ height })
+ this.setState({ height, width })
}
componentWillUnmount() {
@@ -48,26 +50,33 @@ class ComposeExtraButtonList extends React.PureComponent {
edit,
hidePro,
isModal,
- isStandalone,
+ formLocation,
} = this.props
- const { height } = this.state
+ const { height, width } = this.state
- const small = (height <= 660 || isModal) && !isStandalone
+ const isXS = width <= BREAKPOINT_EXTRA_SMALL
+ const isStandalone = formLocation === 'standalone'
+ const isTimeline = formLocation === 'timeline'
+ const small = (!isModal && isXS && !isStandalone) || isTimeline
+
+ console.log("small, formLocation:", small, formLocation)
const containerClasses = CX({
d: 1,
w100PC: 1,
bgPrimary: 1,
- px15: 1,
- py10: 1,
+ px5: 1,
+ py5: 1,
+ mb10: 1,
mtAuto: 1,
- boxShadowBlockY: 1,
- topLeftRadiusSmall: 1,
+ radiusSmall: 1,
+ borderTop1PX: 1,
+ borderBottom1PX: 1,
+ boxShadowBlock: 1,
borderColorSecondary: 1,
- topRightRadiusSmall: 1,
- flexRow: small,
- overflowXScroll: small,
- noScrollbar: small,
+ flexWrap: 1,
+ flexRow: (small || !isTimeline || isXS) && !isStandalone,
+ jcSpaceAround: isXS,
})
return (
@@ -79,8 +88,8 @@ class ComposeExtraButtonList extends React.PureComponent {
{ !hidePro && !edit && }
{ !hidePro && !edit && }
- { !hidePro && }
-
+ { !hidePro && !isXS &&
}
+
)
}
}
@@ -90,7 +99,7 @@ ComposeExtraButtonList.propTypes = {
edit: PropTypes.bool,
isMatch: PropTypes.bool,
isModal: PropTypes.bool,
- isStandalone: PropTypes.bool,
+ formLocation: PropTypes.string,
}
export default ComposeExtraButtonList
diff --git a/app/javascript/gabsocial/features/compose/components/compose_form.js b/app/javascript/gabsocial/features/compose/components/compose_form.js
index 5915a36e..ec27aa32 100644
--- a/app/javascript/gabsocial/features/compose/components/compose_form.js
+++ b/app/javascript/gabsocial/features/compose/components/compose_form.js
@@ -1,5 +1,6 @@
import React from 'react'
import PropTypes from 'prop-types'
+import { NavLink } from 'react-router-dom'
import { defineMessages, injectIntl } from 'react-intl'
import ImmutablePureComponent from 'react-immutable-pure-component'
import ImmutablePropTypes from 'react-immutable-proptypes'
@@ -11,7 +12,6 @@ import {
MAX_POST_CHARACTER_COUNT,
ALLOWED_AROUND_SHORT_CODE,
BREAKPOINT_EXTRA_SMALL,
- BREAKPOINT_EXTRA_EXTRA_SMALL,
BREAKPOINT_MEDIUM,
} from '../../../constants'
import AutosuggestTextbox from '../../../components/autosuggest_textbox'
@@ -62,80 +62,70 @@ class ComposeForm extends ImmutablePureComponent {
}
handleComposeFocus = () => {
- this.setState({
- composeFocused: true,
- });
+ this.setState({ composeFocused: true })
}
handleKeyDown = (e) => {
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
- this.handleSubmit();
+ this.handleSubmit()
}
}
handleClick = (e) => {
- const { isStandalone, isModalOpen, shouldCondense } = this.props
-
+ const { isModalOpen, shouldCondense } = this.props
+
if (!this.form) return false
if (e.target) {
if (e.target.classList.contains('react-datepicker__time-list-item')) return false
}
if (!this.form.contains(e.target)) {
this.handleClickOutside()
- } else {
- // : todo :
- // if mobile go to /compose else openModal
- if (!isStandalone && !isModalOpen && !shouldCondense) {
- this.props.openComposeModal()
- return false
- }
}
}
handleClickOutside = () => {
- const { shouldCondense, scheduledAt, text, isModalOpen } = this.props;
- const condensed = shouldCondense && !text;
+ const { shouldCondense, scheduledAt, text, isModalOpen } = this.props
+ const condensed = shouldCondense && !text
+
if (condensed && scheduledAt && !isModalOpen) { //Reset scheduled date if condensing
- this.props.setScheduledAt(null);
+ this.props.setScheduledAt(null)
}
- this.setState({
- composeFocused: false,
- });
+ this.setState({ composeFocused: false })
}
handleSubmit = () => {
// if (this.props.text !== this.autosuggestTextarea.textbox.value) {
// Something changed the text inside the textarea (e.g. browser extensions like Grammarly)
// Update the state to match the current text
- // this.props.onChange(this.autosuggestTextarea.textbox.value);
+ // this.props.onChange(this.autosuggestTextarea.textbox.value)
// }
// Submit disabled:
const { isSubmitting, isChangingUpload, isUploading, anyMedia, groupId, autoJoinGroup } = this.props
- const fulltext = [this.props.spoilerText, countableText(this.props.text)].join('');
+ const fulltext = [this.props.spoilerText, countableText(this.props.text)].join('')
if (isSubmitting || isUploading || isChangingUpload || length(fulltext) > MAX_POST_CHARACTER_COUNT || (fulltext.length !== 0 && fulltext.trim().length === 0 && !anyMedia)) {
- return;
+ return
}
this.props.onSubmit(groupId, this.props.replyToId, this.context.router, autoJoinGroup)
}
onSuggestionsClearRequested = () => {
- this.props.onClearSuggestions();
+ this.props.onClearSuggestions()
}
onSuggestionsFetchRequested = (token) => {
- this.props.onFetchSuggestions(token);
+ this.props.onFetchSuggestions(token)
}
onSuggestionSelected = (tokenStart, token, value) => {
- this.props.onSuggestionSelected(tokenStart, token, value, ['text']);
+ this.props.onSuggestionSelected(tokenStart, token, value, ['text'])
}
onSpoilerSuggestionSelected = (tokenStart, token, value) => {
- this.props.onSuggestionSelected(tokenStart, token, value, ['spoiler_text']);
+ this.props.onSuggestionSelected(tokenStart, token, value, ['spoiler_text'])
}
handleChangeSpoilerText = (value) => {
@@ -143,11 +133,11 @@ class ComposeForm extends ImmutablePureComponent {
}
componentDidMount() {
- document.addEventListener('click', this.handleClick, false);
+ document.addEventListener('click', this.handleClick, false)
}
componentWillUnmount() {
- document.removeEventListener('click', this.handleClick, false);
+ document.removeEventListener('click', this.handleClick, false)
}
componentDidUpdate(prevProps) {
@@ -156,24 +146,24 @@ class ComposeForm extends ImmutablePureComponent {
// This statement does several things:
// - If we're beginning a reply, and,
// - Replying to zero or one users, places the cursor at the end of the textbox.
- // - Replying to more than one user, selects any usernames past the first;
+ // - Replying to more than one user, selects any usernames past the first
// this provides a convenient shortcut to drop everyone else from the conversation.
if (this.props.focusDate !== prevProps.focusDate) {
- let selectionEnd, selectionStart;
+ let selectionEnd, selectionStart
if (this.props.preselectDate !== prevProps.preselectDate) {
- selectionEnd = this.props.text.length;
- selectionStart = this.props.text.search(/\s/) + 1;
+ selectionEnd = this.props.text.length
+ selectionStart = this.props.text.search(/\s/) + 1
} else if (typeof this.props.caretPosition === 'number') {
- selectionStart = this.props.caretPosition;
- selectionEnd = this.props.caretPosition;
+ selectionStart = this.props.caretPosition
+ selectionEnd = this.props.caretPosition
} else {
- selectionEnd = this.props.text.length;
- selectionStart = selectionEnd;
+ selectionEnd = this.props.text.length
+ selectionStart = selectionEnd
}
- // this.autosuggestTextarea.textbox.setSelectionRange(selectionStart, selectionEnd);
- // this.autosuggestTextarea.textbox.focus();
+ // this.autosuggestTextarea.textbox.setSelectionRange(selectionStart, selectionEnd)
+ // this.autosuggestTextarea.textbox.focus()
}
}
@@ -190,7 +180,6 @@ class ComposeForm extends ImmutablePureComponent {
intl,
account,
onPaste,
- showSearch,
anyMedia,
shouldCondense,
autoFocus,
@@ -208,218 +197,151 @@ class ComposeForm extends ImmutablePureComponent {
isSubmitting,
isPro,
hidePro,
- isStandalone,
+ dontOpenModal,
+ formLocation,
} = this.props
const disabled = isSubmitting
- const text = [this.props.spoilerText, countableText(this.props.text)].join('');
+ const text = [this.props.spoilerText, countableText(this.props.text)].join('')
const disabledButton = isSubmitting || isUploading || isChangingUpload || length(text) > MAX_POST_CHARACTER_COUNT || (length(text.trim()) === 0 && !anyMedia)
- const shouldAutoFocus = autoFocus && !showSearch && !isMobile(window.innerWidth)
+ const shouldAutoFocus = autoFocus && !isMobile(window.innerWidth)
- const parentContainerClasses = CX({
+ const containerClasses = CX({
d: 1,
- w100PC: 1,
- flexRow: !shouldCondense,
- pb10: !shouldCondense,
+ pb10: 1,
+ calcMaxH410PX: 1,
+ minH200PX: isModalOpen && isMatch,
+ overflowYScroll: 1,
+ boxShadowBlock: 1,
+ borderTop1PX: 1,
+ borderColorSecondary: 1,
})
- const childContainerClasses = CX({
- d: 1,
- flexWrap: 1,
- overflowHidden: 1,
- flex1: 1,
- minH28PX: 1,
- py2: shouldCondense,
- aiEnd: shouldCondense,
- flexRow: shouldCondense,
- radiusSmall: shouldCondense,
- bgSubtle: shouldCondense,
- px5: shouldCondense,
- })
-
- const actionsContainerClasses = CX({
- d: 1,
- flexRow: 1,
- aiCenter: !shouldCondense,
- aiStart: shouldCondense,
- mt10: !shouldCondense,
- px10: !shouldCondense,
- mlAuto: shouldCondense,
- flexWrap: !shouldCondense,
- })
-
- const commentPublishBtnClasses = CX({
- d: 1,
- jcCenter: 1,
- displayNone: length(this.props.text) === 0,
- })
+ const textbox = (
+
+ )
if (shouldCondense) {
return (
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+ { textbox }
+ { isMatch &&
}
{
- (isUploading || anyMedia) &&
+ (isUploading || anyMedia) && isMatch &&
-
+
}
)
}
- if (isStandalone || isModalOpen) {
- return (
-
-
-
-
-
-
-
- {
- !!reduxReplyToId && isModalOpen && isMatch &&
-
-
-
- }
-
- {
- !!spoiler &&
-
-
-
- }
-
-
-
- {
- (isUploading || anyMedia) &&
-
-
-
- }
-
- {
- !edit && hasPoll &&
-
- }
-
- {
- !!quoteOfId && isModalOpen && isMatch &&
-
-
-
- }
-
-
-
- { !isModalOpen &&
}
-
-
-
- )
- }
-
return (
-
-
- {intl.formatMessage((shouldCondense || !!reduxReplyToId) && isMatch ? messages.commentPlaceholder : messages.placeholder)}
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+ {
+ !!reduxReplyToId && isModalOpen && isMatch &&
+
+
+
+ }
+
+ {
+ !!spoiler &&
+
+
+
+ }
+
+ { textbox }
+
+ {
+ (isUploading || anyMedia) &&
+
+ }
+
+ {
+ !edit && hasPoll &&
+
+ }
+
+ {
+ !!quoteOfId && isModalOpen && isMatch &&
+
+
+
+ }
+
-
+
+
+
+
+
+ {
+ (!disabledButton || isModalOpen) && isMatch &&
+
+
+
+ }
+
+
+ {
+ formLocation === 'timeline' &&
+
+ }
+
)
}
@@ -450,9 +372,7 @@ ComposeForm.propTypes = {
onFetchSuggestions: PropTypes.func.isRequired,
onSuggestionSelected: PropTypes.func.isRequired,
onChangeSpoilerText: PropTypes.func.isRequired,
- openComposeModal: PropTypes.func.isRequired,
onPaste: PropTypes.func.isRequired,
- showSearch: PropTypes.bool,
anyMedia: PropTypes.bool,
shouldCondense: PropTypes.bool,
autoFocus: PropTypes.bool,
@@ -466,11 +386,7 @@ ComposeForm.propTypes = {
isPro: PropTypes.bool,
hidePro: PropTypes.bool,
autoJoinGroup: PropTypes.bool,
- isStandalone: PropTypes.bool,
-}
-
-ComposeForm.defaultProps = {
- showSearch: false,
+ formLocation: PropTypes.string,
}
export default injectIntl(ComposeForm)
\ No newline at end of file
diff --git a/app/javascript/gabsocial/features/compose/components/compose_form_submit_button.js b/app/javascript/gabsocial/features/compose/components/compose_form_submit_button.js
index 85228701..ecbbc6e1 100644
--- a/app/javascript/gabsocial/features/compose/components/compose_form_submit_button.js
+++ b/app/javascript/gabsocial/features/compose/components/compose_form_submit_button.js
@@ -1,30 +1,80 @@
import React from 'react'
import PropTypes from 'prop-types'
-import { CX } from '../../../constants'
+import { connect } from 'react-redux'
+import { defineMessages, injectIntl } from 'react-intl'
+import { length } from 'stringz'
+import { countableText } from '../../ui/util/counter'
+import { submitCompose } from '../../../actions/compose'
+import {
+ CX,
+ MAX_POST_CHARACTER_COUNT,
+} from '../../../constants'
import Button from '../../../components/button'
import Text from '../../../components/text'
class ComposeFormSubmitButton extends React.PureComponent {
+ handleSubmit = () => {
+
+ }
+
render() {
const {
+ intl,
title,
active,
small,
- disabledButton,
type,
+
+ edit,
+ text,
+ isSubmitting,
+ isChangingUpload,
+ isUploading,
+ anyMedia,
+ quoteOfId,
+ scheduledAt,
+ hasPoll,
} = this.props
+ const disabledButton = isSubmitting || isUploading || isChangingUpload || length(text) > MAX_POST_CHARACTER_COUNT || (length(text.trim()) === 0 && !anyMedia)
+
+ if (type === 'comment') {
+ const commentPublishBtnClasses = CX({
+ d: 1,
+ jcCenter: 1,
+ displayNone: disabledButton,
+ })
+
+ return (
+
+
+
+
+
+
+
+
+ )
+ }
+
const containerClasses = CX({
d: 1,
- mr5: 1,
jcCenter: 1,
h40PX: 1,
})
const btnClasses = CX({
d: 1,
- circle: 1,
+ radiusSmall: 1,
noUnderline: 1,
font: 1,
cursorPointer: 1,
@@ -37,31 +87,33 @@ class ComposeFormSubmitButton extends React.PureComponent {
py10: 1,
px10: 1,
})
-
- const iconClasses = CX({
- cSecondary: !active,
- cWhite: active,
- mr10: 1,
- py2: small,
- ml10: small,
- })
-
- const iconSize = !small ? '18px' : '16px'
- const textColor = !active ? 'primary' : 'white'
-
+
+ let backgroundColor, color
+ if (disabledButton) {
+ backgroundColor = 'tertiary'
+ color = 'tertiary'
+ } else if (type === 'navigation') {
+ backgroundColor = 'white'
+ color = 'brand'
+ } else {
+ backgroundColor = 'brand'
+ color = 'white'
+ }
+
return (
-
+
@@ -71,9 +123,32 @@ class ComposeFormSubmitButton extends React.PureComponent {
}
-// {intl.formatMessage(scheduledAt ? messages.schedulePost : edit ? messages.postEdit : messages.post)}
+const messages = defineMessages({
+ post: { id: 'compose_form.post', defaultMessage: 'Post' },
+ postEdit: { id: 'compose_form.post_edit', defaultMessage: 'Post Edit' },
+ schedulePost: { id: 'compose_form.schedule_post', defaultMessage: 'Schedule Post' },
+})
+
+const mapStateToProps = (state) => ({
+ edit: state.getIn(['compose', 'id']) !== null,
+ text: state.getIn(['compose', 'text']),
+ isSubmitting: state.getIn(['compose', 'is_submitting']),
+ isChangingUpload: state.getIn(['compose', 'is_changing_upload']),
+ isUploading: state.getIn(['compose', 'is_uploading']),
+ anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
+ quoteOfId: state.getIn(['compose', 'quote_of_id']),
+ scheduledAt: state.getIn(['compose', 'scheduled_at']),
+ hasPoll: state.getIn(['compose', 'poll']),
+})
+
+const mapDispatchToProps = (dispatch) => ({
+ onSubmit(groupId, replyToId = null, router, isStandalone, autoJoinGroup) {
+ dispatch(submitCompose(groupId, replyToId, router, isStandalone, autoJoinGroup))
+ }
+})
+
ComposeFormSubmitButton.propTypes = {
- type: PropTypes.oneOf(['header', 'block', 'comment'])
+ type: PropTypes.oneOf(['header', 'navigation', 'block', 'comment'])
}
-export default ComposeFormSubmitButton
\ No newline at end of file
+export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ComposeFormSubmitButton))
\ No newline at end of file
diff --git a/app/javascript/gabsocial/features/compose/components/expires_post_button.js b/app/javascript/gabsocial/features/compose/components/expires_post_button.js
index 9fda68e1..e5b814aa 100644
--- a/app/javascript/gabsocial/features/compose/components/expires_post_button.js
+++ b/app/javascript/gabsocial/features/compose/components/expires_post_button.js
@@ -48,7 +48,7 @@ class ExpiresPostButton extends React.PureComponent {
}
const messages = defineMessages({
- expires: { id: 'expiration.title', defaultMessage: 'Add status expiration' },
+ expires: { id: 'expiration.title', defaultMessage: 'Status expiration' },
})
const mapStateToProps = (state) => ({
diff --git a/app/javascript/gabsocial/features/compose/components/media_upload_item.js b/app/javascript/gabsocial/features/compose/components/media_upload_item.js
index bbae0a33..09d38357 100644
--- a/app/javascript/gabsocial/features/compose/components/media_upload_item.js
+++ b/app/javascript/gabsocial/features/compose/components/media_upload_item.js
@@ -19,7 +19,7 @@ class Upload extends ImmutablePureComponent {
}
state = {
- hovered: false,
+ hovering: false,
focused: false,
dirtyDescription: null,
}
@@ -45,11 +45,11 @@ class Upload extends ImmutablePureComponent {
}
handleMouseEnter = () => {
- this.setState({ hovered: true })
+ this.setState({ hovering: true })
}
handleMouseLeave = () => {
- this.setState({ hovered: false })
+ this.setState({ hovering: false })
}
handleInputFocus = () => {
@@ -75,66 +75,60 @@ class Upload extends ImmutablePureComponent {
render() {
const { intl, media } = this.props
- const active = this.state.hovered || this.state.focused
- const description = this.state.dirtyDescription || (this.state.dirtyDescription !== '' && media.get('description')) || ''
+ const { hovering } = this.state
- const descriptionContainerClasses = CX({
- d: 1,
- posAbs: 1,
- right0: 1,
- bottom0: 1,
- left0: 1,
- mt5: 1,
- mb5: 1,
- ml5: 1,
- mr5: 1,
- displayNone: !active,
- })
+ const active = hovering || this.state.focused
+ const description = this.state.dirtyDescription || (this.state.dirtyDescription !== '' && media.get('description')) || ''
return (
-
+
+ { hovering &&
}
{
media.get('type') === 'gifv' &&
-
diff --git a/app/javascript/gabsocial/features/compose/components/sensitive_media_button.js b/app/javascript/gabsocial/features/compose/components/sensitive_media_button.js
index a5e15e08..cebc9d5f 100644
--- a/app/javascript/gabsocial/features/compose/components/sensitive_media_button.js
+++ b/app/javascript/gabsocial/features/compose/components/sensitive_media_button.js
@@ -11,7 +11,7 @@ class SensitiveMediaButton extends React.PureComponent {
const { active, disabled, onClick, intl } = this.props
return (
-
+
+ { isUploading && }
+
- {
- mediaIds.map(id => (
-
- ))
- }
+ {mediaIds.map(id => (
+
+ ))}
- {
- !mediaIds.isEmpty() &&
-
- }
-
- {
- isUploading &&
-
- }
+ { !mediaIds.isEmpty() && }
+ { isUploading && }
)
}
@@ -48,7 +41,6 @@ const mapStateToProps = (state) => ({
})
UploadForm.propTypes = {
- isModalOpen: PropTypes.bool,
isUploading: PropTypes.bool,
mediaIds: ImmutablePropTypes.list.isRequired,
uploadProgress: PropTypes.number,
diff --git a/app/javascript/gabsocial/features/compose/compose.js b/app/javascript/gabsocial/features/compose/compose.js
index 0d2cc9ab..8d016880 100644
--- a/app/javascript/gabsocial/features/compose/compose.js
+++ b/app/javascript/gabsocial/features/compose/compose.js
@@ -11,7 +11,7 @@ class Compose extends React.PureComponent {
}
render () {
- return
+ return
}
}
diff --git a/app/javascript/gabsocial/features/introduction.js b/app/javascript/gabsocial/features/introduction.js
index 12ef317e..b813298c 100644
--- a/app/javascript/gabsocial/features/introduction.js
+++ b/app/javascript/gabsocial/features/introduction.js
@@ -186,9 +186,9 @@ class SlideFirstPost extends React.PureComponent {
@@ -325,7 +325,7 @@ class Introduction extends ImmutablePureComponent {
)
diff --git a/app/javascript/gabsocial/features/messages/components/chat_message_compose_form.js b/app/javascript/gabsocial/features/messages/components/chat_message_compose_form.js
index cd55ccdc..e8f3db69 100644
--- a/app/javascript/gabsocial/features/messages/components/chat_message_compose_form.js
+++ b/app/javascript/gabsocial/features/messages/components/chat_message_compose_form.js
@@ -8,6 +8,7 @@ import { openModal } from '../../../actions/modal'
import { sendChatMessage } from '../../../actions/chat_messages'
import { CX } from '../../../constants'
import Button from '../../../components/button'
+import Icon from '../../../components/icon'
import Input from '../../../components/input'
import Text from '../../../components/text'
@@ -23,6 +24,10 @@ class ChatMessagesComposeForm extends React.PureComponent {
this.setState({ value: '' })
}
+ handleOnExpire = () => {
+ //
+ }
+
onChange = (e) => {
this.setState({ value: e.target.value })
}
@@ -68,6 +73,10 @@ class ChatMessagesComposeForm extends React.PureComponent {
this.sendBtn = c
}
+ setExpiresBtn = (c) => {
+ this.expiresBtn = c
+ }
+
render () {
const { isXS, chatConversationId } = this.props
const { value } = this.state
@@ -85,9 +94,7 @@ class ChatMessagesComposeForm extends React.PureComponent {
px10: 1,
fs14PX: 1,
maxH200PX: 1,
- borderColorSecondary: 1,
- border1PX: 1,
- radiusRounded: 1,
+ w100PC: 1,
py10: 1,
})
@@ -105,6 +112,7 @@ class ChatMessagesComposeForm extends React.PureComponent {
onBlur={this.onBlur}
onKeyDown={this.onKeyDown}
aria-autocomplete='list'
+ maxLength={1600}
/>
)
@@ -114,18 +122,33 @@ class ChatMessagesComposeForm extends React.PureComponent {
disabled={disabled}
onClick={this.handleOnSendChatMessage}
>
-
Send
+
Send
)
+ const expiresBtn = (
+
+ )
+
if (isXS) {
return (
-
- {textarea}
+
+
+ {expiresBtn}
+
+
+ {textarea}
+
{button}
@@ -140,9 +163,16 @@ class ChatMessagesComposeForm extends React.PureComponent {
return (
- {textarea}
+
+
+ {expiresBtn}
+
+
+ {textarea}
+
+
-
@@ -163,4 +193,4 @@ ChatMessagesComposeForm.propTypes = {
onSendMessage: PropTypes.func.isRequired,
}
-export default connect(null, mapDispatchToProps)(ChatMessagesComposeForm)
\ No newline at end of file
+export default connect(mapDispatchToProps)(ChatMessagesComposeForm)
\ No newline at end of file
diff --git a/app/javascript/gabsocial/features/messages/components/chat_message_item.js b/app/javascript/gabsocial/features/messages/components/chat_message_item.js
index 05a29d41..0e2234ec 100644
--- a/app/javascript/gabsocial/features/messages/components/chat_message_item.js
+++ b/app/javascript/gabsocial/features/messages/components/chat_message_item.js
@@ -8,7 +8,7 @@ import { NavLink } from 'react-router-dom'
import { openPopover } from '../../../actions/popover'
import {
CX,
- POPOVER_CHAT_MESSAGE_DELETE,
+ POPOVER_CHAT_MESSAGE_OPTIONS,
} from '../../../constants'
import { me } from '../../../initial_state'
import Input from '../../../components/input'
@@ -51,7 +51,7 @@ class ChatMessageItem extends ImmutablePureComponent {
}
handleMoreClick = () => {
- this.props.onOpenChatMessageDeletePopover(this.props.chatMessageId, this.deleteBtnRef)
+ this.props.onOpenChatMessageOptionsPopover(this.props.chatMessageId, this.deleteBtnRef)
}
setDeleteBtnRef = (c) => {
@@ -122,7 +122,7 @@ class ChatMessageItem extends ImmutablePureComponent {
const buttonContainerClasses = CX({
d: 1,
flexRow: 1,
- displayNone: !isHovering && alt,
+ displayNone: !isHovering,
})
return (
@@ -145,19 +145,16 @@ class ChatMessageItem extends ImmutablePureComponent {
- {
- alt &&
-
-
-
- }
+
+
+
@@ -178,8 +175,8 @@ const mapStateToProps = (state, { lastChatMessageId, chatMessageId }) => ({
})
const mapDispatchToProps = (dispatch) => ({
- onOpenChatMessageDeletePopover(chatMessageId, targetRef) {
- dispatch(openPopover(POPOVER_CHAT_MESSAGE_DELETE, {
+ onOpenChatMessageOptionsPopover(chatMessageId, targetRef) {
+ dispatch(openPopover(POPOVER_CHAT_MESSAGE_OPTIONS, {
targetRef,
chatMessageId,
position: 'top',
diff --git a/app/javascript/gabsocial/features/messages/components/chat_message_scrolling_list.js b/app/javascript/gabsocial/features/messages/components/chat_message_scrolling_list.js
index 2ff88a80..8f179df1 100644
--- a/app/javascript/gabsocial/features/messages/components/chat_message_scrolling_list.js
+++ b/app/javascript/gabsocial/features/messages/components/chat_message_scrolling_list.js
@@ -15,6 +15,7 @@ import {
expandChatMessages,
scrollBottomChatMessageConversation,
} from '../../../actions/chat_conversation_messages'
+import { readChatConversation } from '../../../actions/chat_conversations'
import IntersectionObserverArticle from '../../../components/intersection_observer_article'
import IntersectionObserverWrapper from '../../ui/util/intersection_observer_wrapper'
import ChatMessagePlaceholder from '../../../components/placeholder/chat_message_placeholder'
@@ -58,7 +59,6 @@ class ChatMessageScrollingList extends ImmutablePureComponent {
// Reset the scroll position when a new child comes in in order not to
// jerk the scrollbar around if you're already scrolled down the page.
if (snapshot !== null && this.scrollContainerRef) {
- console.log("snapshot:", snapshot)
this.setScrollTop(this.scrollContainerRef.scrollHeight - snapshot)
}
@@ -68,6 +68,7 @@ class ChatMessageScrollingList extends ImmutablePureComponent {
if (prevProps.chatMessageIds.size === 0 && this.props.chatMessageIds.size > 0 && this.scrollContainerRef) {
this.scrollContainerRef.scrollTop = this.scrollContainerRef.scrollHeight
+ this.props.onReadChatConversation(this.props.chatConversationId)
}
}
@@ -363,6 +364,9 @@ const mapDispatchToProps = (dispatch, ownProps) => ({
onSetChatConversationSelected: (chatConversationId) => {
dispatch(setChatConversationSelected(chatConversationId))
},
+ onReadChatConversation(chatConversationId) {
+ dispatch(readChatConversation(chatConversationId))
+ },
})
ChatMessageScrollingList.propTypes = {
diff --git a/app/javascript/gabsocial/features/ui/ui.js b/app/javascript/gabsocial/features/ui/ui.js
index b18ef758..d28a8b51 100644
--- a/app/javascript/gabsocial/features/ui/ui.js
+++ b/app/javascript/gabsocial/features/ui/ui.js
@@ -52,11 +52,13 @@ import DeckPage from '../../pages/deck_page'
import {
About,
+ AccountAlbums,
AccountGallery,
AccountTimeline,
AccountCommentsTimeline,
Assets,
BlockedAccounts,
+ BookmarkCollections,
BookmarkedStatuses,
CaliforniaConsumerProtection,
CaliforniaConsumerProtectionContact,
@@ -274,9 +276,11 @@ class SwitchingArea extends React.PureComponent {
+
-
+
+
diff --git a/app/javascript/gabsocial/features/ui/util/async_components.js b/app/javascript/gabsocial/features/ui/util/async_components.js
index 8e0cde4d..24667426 100644
--- a/app/javascript/gabsocial/features/ui/util/async_components.js
+++ b/app/javascript/gabsocial/features/ui/util/async_components.js
@@ -6,6 +6,7 @@ export function AccountGallery() { return import(/* webpackChunkName: "features/
export function Assets() { return import(/* webpackChunkName: "features/about/assets" */'../../about/assets') }
export function BlockAccountModal() { return import(/* webpackChunkName: "components/block_account_modal" */'../../../components/modal/block_account_modal') }
export function BlockedAccounts() { return import(/* webpackChunkName: "features/blocked_accounts" */'../../blocked_accounts') }
+export function BookmarkCollections() { return import(/* webpackChunkName: "features/bookmark_collections" */'../../bookmark_collections') }
export function BookmarkedStatuses() { return import(/* webpackChunkName: "features/bookmarked_statuses" */'../../bookmarked_statuses') }
export function BoostModal() { return import(/* webpackChunkName: "components/boost_modal" */'../../../components/modal/boost_modal') }
export function CaliforniaConsumerProtection() { return import(/* webpackChunkName: "features/california_consumer_protection" */'../../about/california_consumer_protection') }
@@ -17,17 +18,19 @@ export function ChatConversationDeleteModal() { return import(/* webpackChunkNam
export function ChatConversationMutedAccounts() { return import(/* webpackChunkName: "features/chat_conversation_muted_accounts" */'../../chat_conversation_muted_accounts') }
export function ChatConversationOptionsPopover() { return import(/* webpackChunkName: "components/chat_conversation_options_popover" */'../../../components/popover/chat_conversation_options_popover') }
export function ChatConversationRequests() { return import(/* webpackChunkName: "features/chat_conversation_requests" */'../../chat_conversation_requests') }
-export function ChatMessageDeletePopover() { return import(/* webpackChunkName: "components/chat_message_delete_popover" */'../../../components/popover/chat_message_delete_popover') }
+export function ChatMessageOptionsPopover() { return import(/* webpackChunkName: "components/chat_message_options_popover" */'../../../components/popover/chat_message_options_popover') }
export function CommentSortingOptionsPopover() { return import(/* webpackChunkName: "components/comment_sorting_options_popover" */'../../../components/popover/comment_sorting_options_popover') }
export function CommunityTimeline() { return import(/* webpackChunkName: "features/community_timeline" */'../../community_timeline') }
export function CommunityTimelineSettingsModal() { return import(/* webpackChunkName: "components/community_timeline_settings_modal" */'../../../components/modal/community_timeline_settings_modal') }
export function Compose() { return import(/* webpackChunkName: "features/compose" */'../../compose') }
export function ComposeForm() { return import(/* webpackChunkName: "components/compose_form" */'../../compose/components/compose_form') }
export function ComposeModal() { return import(/* webpackChunkName: "components/compose_modal" */'../../../components/modal/compose_modal') }
+export function ComposePostDesinationPopover() { return import(/* webpackChunkName: "components/compose_post_destination_popover" */'../../../components/popover/compose_post_destination_popover') }
export function ConfirmationModal() { return import(/* webpackChunkName: "components/confirmation_modal" */'../../../components/modal/confirmation_modal') }
export function DatePickerPopover() { return import(/* webpackChunkName: "components/date_picker_popover" */'../../../components/popover/date_picker_popover') }
export function Deck() { return import(/* webpackChunkName: "features/deck" */'../../deck') }
export function DeckColumnAddModal() { return import(/* webpackChunkName: "components/deck_column_add_modal" */'../../../components/modal/deck_column_add_modal') }
+export function DeckColumnAddOptionsModal() { return import(/* webpackChunkName: "components/deck_column_add_options_modal" */'../../../components/modal/deck_column_add_options_modal') }
export function DisplayOptionsModal() { return import(/* webpackChunkName: "components/display_options_modal" */'../../../components/modal/display_options_modal') }
export function DMCA() { return import(/* webpackChunkName: "features/about/dmca" */'../../about/dmca') }
export function EditProfileModal() { return import(/* webpackChunkName: "components/edit_profile_modal" */'../../../components/modal/edit_profile_modal') }
diff --git a/app/javascript/gabsocial/initial_state.js b/app/javascript/gabsocial/initial_state.js
index 9b6faa1b..90ba9805 100644
--- a/app/javascript/gabsocial/initial_state.js
+++ b/app/javascript/gabsocial/initial_state.js
@@ -20,6 +20,7 @@ export const isStaff = getMeta('is_staff');
export const unreadCount = getMeta('unread_count');
export const lastReadNotificationId = getMeta('last_read_notification_id');
export const monthlyExpensesComplete = getMeta('monthly_expenses_complete');
+export const trendingHashtags = getMeta('trending_hashtags');
export const isFirstSession = getMeta('is_first_session');
export const emailConfirmed = getMeta('email_confirmed');
export const meEmail = getMeta('email');
diff --git a/app/javascript/gabsocial/layouts/deck_layout.js b/app/javascript/gabsocial/layouts/deck_layout.js
index 371cb272..5b7844b4 100644
--- a/app/javascript/gabsocial/layouts/deck_layout.js
+++ b/app/javascript/gabsocial/layouts/deck_layout.js
@@ -5,13 +5,48 @@ import {
BREAKPOINT_EXTRA_SMALL,
} from '../constants'
import { me } from '../initial_state'
+import Button from '../components/button'
+import Text from '../components/text'
import DeckSidebar from '../components/sidebar/deck_sidebar'
import WrappedBundle from '../features/ui/util/wrapped_bundle'
+import { getWindowDimension } from '../utils/is_mobile'
+
+const initialState = getWindowDimension()
class DeckLayout extends React.PureComponent {
+ state = {
+ width: initialState.width,
+ }
+
+ componentDidMount() {
+ this.handleResize()
+ window.addEventListener('resize', this.handleResize, false)
+ }
+
+ componentWillUnmount() {
+ window.removeEventListener('resize', this.handleResize, false)
+ }
+
+ handleResize = () => {
+ const { width } = getWindowDimension()
+ this.setState({ width })
+ }
+
render() {
const { children, title } = this.props
+ const { width } = this.state
+
+ const isXS = width <= BREAKPOINT_EXTRA_SMALL
+
+ if (isXS) {
+ return (
+
+ Gab Deck is not available on mobile or tablet devices. Please only access using a desktop computer.
+
+
+ )
+ }
const mainBlockClasses = CX({
d: 1,
diff --git a/app/javascript/gabsocial/layouts/search_layout.js b/app/javascript/gabsocial/layouts/search_layout.js
index 71d0a720..c226aafb 100644
--- a/app/javascript/gabsocial/layouts/search_layout.js
+++ b/app/javascript/gabsocial/layouts/search_layout.js
@@ -2,7 +2,7 @@ import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { defineMessages, injectIntl } from 'react-intl'
-import { me } from '../initial_state'
+import { me, trendingHashtags } from '../initial_state'
import {
BREAKPOINT_EXTRA_SMALL,
CX,
@@ -42,26 +42,20 @@ class SearchLayout extends React.PureComponent {
title: 'Explore',
onClick: () => this.setState({ currentExploreTabIndex: 0 }),
component: ExploreTimeline,
- },
- {
- title: '#Election2020',
- onClick: () => this.setState({ currentExploreTabIndex: 1 }),
- component: HashtagTimeline,
- componentParams: { params: { id: 'election2020' } },
- },
- {
- title: '#RiggedElection',
- onClick: () => this.setState({ currentExploreTabIndex: 2 }),
- component: HashtagTimeline,
- componentParams: { params: { id: 'riggedelection' } },
- },
- {
- title: '#StopTheSteal',
- onClick: () => this.setState({ currentExploreTabIndex: 3 }),
- component: HashtagTimeline,
- componentParams: { params: { id: 'stopthesteal' } },
- },
+ }
]
+
+ if (Array.isArray(trendingHashtags)) {
+ trendingHashtags.forEach((tag, i) => {
+ let j = i + 1
+ this.exploreTabs.push({
+ title: `#${tag}`,
+ onClick: () => this.setState({ currentExploreTabIndex: j }),
+ component: HashtagTimeline,
+ componentParams: { params: { id: `${tag}`.toLowerCase() } },
+ })
+ })
+ }
this.searchTabs = [
{
diff --git a/app/javascript/gabsocial/reducers/albums.js b/app/javascript/gabsocial/reducers/albums.js
new file mode 100644
index 00000000..384ee9a9
--- /dev/null
+++ b/app/javascript/gabsocial/reducers/albums.js
@@ -0,0 +1,62 @@
+import {
+ BOOKMARK_COLLECTIONS_FETCH_REQUEST,
+ BOOKMARK_COLLECTIONS_FETCH_SUCCESS,
+ BOOKMARK_COLLECTIONS_FETCH_FAIL,
+} from '../actions/bookmarks'
+import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'
+
+const initialState = ImmutableMap({
+ items: ImmutableList(),
+ isLoading: false,
+ isFetched: false,
+ isError: false,
+})
+
+const normalizeBookmarkCollection = (bookmarkCollection) => {
+ return {
+ id: shortcut.id,
+ shortcut_type: 'account',
+ shortcut_id: shortcut.shortcut_id,
+ title: shortcut.shortcut.acct,
+ image: shortcut.shortcut.avatar_static,
+ to: `/${shortcut.shortcut.acct}`,
+ }
+}
+
+const normalizeBookmarkCollections = (shortcuts) => {
+ return fromJS(shortcuts.map((shortcut) => {
+ return normalizeShortcut(shortcut)
+ }))
+}
+
+export default function albums(state = initialState, action) {
+ switch(action.type) {
+ case SHORTCUTS_FETCH_REQUEST:
+ return state.withMutations((map) => {
+ map.set('isLoading', true)
+ map.set('isFetched', false)
+ map.set('isError', false)
+ })
+ case SHORTCUTS_FETCH_SUCCESS:
+ return state.withMutations((map) => {
+ map.set('items', normalizeShortcuts(action.shortcuts))
+ map.set('isLoading', false)
+ map.set('isFetched', true)
+ map.set('isError', false)
+ })
+ case SHORTCUTS_FETCH_FAIL:
+ return state.withMutations((map) => {
+ map.set('isLoading', false)
+ map.set('isFetched', true)
+ map.set('isError', true)
+ })
+ case BOOKMARK_COLLECTIONS_CREATE_REQUEST:
+ return state.update('items', list => list.push(fromJS(normalizeShortcut(action.shortcut))))
+ case BOOKMARK_COLLECTIONS_REMOVE_REQUEST:
+ return state.update('items', list => list.filterNot((item) => {
+ return `${item.get('id')}` === `${action.shortcutId}`
+ }))
+ default:
+ return state
+ }
+}
diff --git a/app/javascript/gabsocial/reducers/bookmark_collections.js b/app/javascript/gabsocial/reducers/bookmark_collections.js
new file mode 100644
index 00000000..ab48880f
--- /dev/null
+++ b/app/javascript/gabsocial/reducers/bookmark_collections.js
@@ -0,0 +1,62 @@
+import {
+ BOOKMARK_COLLECTIONS_FETCH_REQUEST,
+ BOOKMARK_COLLECTIONS_FETCH_SUCCESS,
+ BOOKMARK_COLLECTIONS_FETCH_FAIL,
+} from '../actions/bookmarks'
+import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'
+
+const initialState = ImmutableMap({
+ items: ImmutableList(),
+ isLoading: false,
+ isFetched: false,
+ isError: false,
+})
+
+const normalizeBookmarkCollection = (bookmarkCollection) => {
+ return {
+ id: shortcut.id,
+ shortcut_type: 'account',
+ shortcut_id: shortcut.shortcut_id,
+ title: shortcut.shortcut.acct,
+ image: shortcut.shortcut.avatar_static,
+ to: `/${shortcut.shortcut.acct}`,
+ }
+}
+
+const normalizeBookmarkCollections = (shortcuts) => {
+ return fromJS(shortcuts.map((shortcut) => {
+ return normalizeShortcut(shortcut)
+ }))
+}
+
+export default function bookmark_collections(state = initialState, action) {
+ switch(action.type) {
+ case SHORTCUTS_FETCH_REQUEST:
+ return state.withMutations((map) => {
+ map.set('isLoading', true)
+ map.set('isFetched', false)
+ map.set('isError', false)
+ })
+ case SHORTCUTS_FETCH_SUCCESS:
+ return state.withMutations((map) => {
+ map.set('items', normalizeShortcuts(action.shortcuts))
+ map.set('isLoading', false)
+ map.set('isFetched', true)
+ map.set('isError', false)
+ })
+ case SHORTCUTS_FETCH_FAIL:
+ return state.withMutations((map) => {
+ map.set('isLoading', false)
+ map.set('isFetched', true)
+ map.set('isError', true)
+ })
+ case BOOKMARK_COLLECTIONS_CREATE_REQUEST:
+ return state.update('items', list => list.push(fromJS(normalizeShortcut(action.shortcut))))
+ case BOOKMARK_COLLECTIONS_REMOVE_REQUEST:
+ return state.update('items', list => list.filterNot((item) => {
+ return `${item.get('id')}` === `${action.shortcutId}`
+ }))
+ default:
+ return state
+ }
+}
diff --git a/app/javascript/gabsocial/reducers/chat_conversations.js b/app/javascript/gabsocial/reducers/chat_conversations.js
index d2adccbc..83f5e6c7 100644
--- a/app/javascript/gabsocial/reducers/chat_conversations.js
+++ b/app/javascript/gabsocial/reducers/chat_conversations.js
@@ -7,6 +7,7 @@ import { me } from '../initial_state'
import {
CHAT_MESSAGES_SEND_SUCCESS,
CHAT_MESSAGES_DELETE_REQUEST,
+ CHAT_MESSAGES_PURGE_REQUEST,
} from '../actions/chat_messages'
import {
CHAT_CONVERSATIONS_APPROVED_FETCH_SUCCESS,
@@ -14,6 +15,7 @@ import {
CHAT_CONVERSATIONS_REQUESTED_FETCH_SUCCESS,
CHAT_CONVERSATIONS_REQUESTED_EXPAND_SUCCESS,
CHAT_CONVERSATION_REQUEST_APPROVE_SUCCESS,
+ CHAT_CONVERSATION_MARK_READ_SUCCESS,
} from '../actions/chat_conversations'
const initialState = ImmutableMap()
@@ -50,6 +52,11 @@ export default function chat_conversations(state = initialState, action) {
case CHAT_MESSAGES_DELETE_REQUEST:
// : todo : set last conversation message to one prior to this one
return state
+ case CHAT_MESSAGES_PURGE_REQUEST:
+ // : todo :
+ return state
+ case CHAT_CONVERSATION_MARK_READ_SUCCESS:
+ return importChatConversation(state, action.chatConversation)
default:
return state
}
diff --git a/app/javascript/gabsocial/reducers/chat_messages.js b/app/javascript/gabsocial/reducers/chat_messages.js
index 4f3e5647..f2855cae 100644
--- a/app/javascript/gabsocial/reducers/chat_messages.js
+++ b/app/javascript/gabsocial/reducers/chat_messages.js
@@ -2,6 +2,7 @@ import { Map as ImmutableMap, fromJS } from 'immutable'
import {
CHAT_MESSAGES_SEND_SUCCESS,
CHAT_MESSAGES_DELETE_REQUEST,
+ CHAT_MESSAGES_PURGE_REQUEST,
} from '../actions/chat_messages'
import {
CHAT_MESSAGES_IMPORT,
@@ -26,6 +27,8 @@ export default function chat_messages(state = initialState, action) {
return importChatMessage(state, action.chatMessage)
case CHAT_MESSAGES_DELETE_REQUEST:
return deleteChatMessage(state, action.chatMessageId)
+ case CHAT_MESSAGES_PURGE_REQUEST:
+ return state
default:
return state
}
diff --git a/app/javascript/gabsocial/reducers/chats.js b/app/javascript/gabsocial/reducers/chats.js
index e1c077bf..a2671f56 100644
--- a/app/javascript/gabsocial/reducers/chats.js
+++ b/app/javascript/gabsocial/reducers/chats.js
@@ -11,6 +11,7 @@ import {
import {
CHAT_CONVERSATION_APPROVED_UNREAD_COUNT_FETCH_SUCCESS,
CHAT_CONVERSATION_REQUESTED_COUNT_FETCH_SUCCESS,
+ CHAT_CONVERSATION_MARK_READ_FETCH,
} from '../actions/chat_conversations'
import {
CHAT_MESSAGES_FETCH_SUCCESS,
@@ -34,6 +35,10 @@ export default function chats(state = initialState, action) {
return state.set('chatConversationRequestCount', action.count)
case CHAT_CONVERSATION_APPROVED_UNREAD_COUNT_FETCH_SUCCESS:
return state.set('chatsUnreadCount', action.count)
+ case CHAT_CONVERSATION_MARK_READ_FETCH:
+ const chatConversationUnreadCount = action.chatConversation.get('unread_count')
+ const totalUnreadCount = state.get('chatsUnreadCount')
+ return state.set('chatsUnreadCount', Math.max(totalUnreadCount - chatConversationUnreadCount, 0))
default:
return state
}
diff --git a/app/javascript/styles/global.css b/app/javascript/styles/global.css
index f3046193..5aecc1dc 100644
--- a/app/javascript/styles/global.css
+++ b/app/javascript/styles/global.css
@@ -360,6 +360,7 @@ pre {
.circle { border-radius: var(--radius-circle); }
.radiusSmall { border-radius: var(--radius-small); }
.radiusRounded { border-radius: var(--radius-rounded); }
+
.topLeftRadiusSmall { border-top-left-radius: var(--radius-small); }
.topRightRadiusSmall { border-top-right-radius: var(--radius-small); }
.bottomRightRadiusSmall { border-bottom-right-radius: var(--radius-small); }
@@ -443,6 +444,7 @@ pre {
.bgBlack { background-color: var(--color_black); }
.bgBlackOpaque { background-color: var(--color_black-opaquer); }
+.bgBlackOpaquest { background-color: var(--color_black-opaquest); }
.bgBlackOpaque_onHover:hover { background-color: var(--color_black-opaque); }
.bgBlackOpaquest_onHover:hover { background-color: var(--color_black-opaquest); }
@@ -550,10 +552,9 @@ pre {
.calcH53PX { height: calc(100vh - 53px); }
.calcH80VH106PX { height: calc(80vh - 106px); }
-.calcMaxH370PX { max-height: calc(100vh - 370px); }
-@media (min-height: 0px) and (max-height:660px) {
- .calcMaxH370PX { max-height: calc(100vh - 140px); }
-}
+.calcMaxH410PX { max-height: calc(100vh - 450px); }
+@media (min-width: 0px) and (max-width:992) { .calcMaxH410PX { max-height: calc(100vh - 410px); } }
+@media (min-height: 0px) and (max-height:660px) { .calcMaxH410PX { max-height: calc(100vh - 140px); } }
.minH100VH { min-height: 100vh; }
.minH50VH { min-height: 50vh; }
@@ -841,6 +842,7 @@ pre {
.mt5 { margin-top: 5px; }
.mt2 { margin-top: 2px; }
.mtAuto { margin-top: auto; }
+.mtNeg5PX { margin-top: -5px; }
.mtNeg26PX { margin-top: -26px; }
.mtNeg32PX { margin-top: -32px; }
.mtNeg50PX { margin-top: -50px; }
@@ -872,6 +874,7 @@ pre {
.pl0 { padding-left: 0; }
.pr50 { padding-right: 50px; }
+.pr20 { padding-right: 20px; }
.pr15 { padding-right: 15px; }
.pr10 { padding-right: 10px; }
.pr5 { padding-right: 5px; }
diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb
index 43348c91..72d1d2cf 100644
--- a/app/lib/feed_manager.rb
+++ b/app/lib/feed_manager.rb
@@ -132,7 +132,7 @@ class FeedManager
private
def push_update_required?(timeline_id)
- redis.exists("subscribed:#{timeline_id}")
+ redis.exists?("subscribed:#{timeline_id}")
end
def blocks_or_mutes?(receiver_id, account_ids, context)
diff --git a/app/models/account.rb b/app/models/account.rb
index 9a5bc3ec..de95feee 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -50,6 +50,7 @@
# is_verified :boolean default(FALSE), not null
# is_donor :boolean default(FALSE), not null
# is_investor :boolean default(FALSE), not null
+# is_flagged_as_spam :boolean default(FALSE), not null
#
class Account < ApplicationRecord
@@ -91,7 +92,7 @@ class Account < ApplicationRecord
scope :recent, -> { reorder(id: :desc) }
scope :bots, -> { where(actor_type: %w(Application Service)) }
scope :alphabetic, -> { order(domain: :asc, username: :asc) }
- scope :by_domain_accounts, -> { group(:domain).select(:domain, 'COUNT(*) AS accounts_count').order('accounts_count desc') }
+ scope :by_domain_accounts, -> { group(:id).select(:domain, 'COUNT(*) AS accounts_count').order('accounts_count desc') }
scope :matches_username, ->(value) { where(arel_table[:username].matches("#{value}%")) }
scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) }
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
@@ -148,6 +149,14 @@ class Account < ApplicationRecord
Follow.where(target_account_id: id).count
end
+ def chat_conversation_accounts_count
+ ChatConversationAccount.where(account_id: id).count
+ end
+
+ def chat_messages_count
+ ChatMessage.where(from_account_id: id).count
+ end
+
def silenced?
silenced_at.present?
end
diff --git a/app/models/account_conversation.rb b/app/models/account_conversation.rb
index b555eaa4..2d4debd3 100644
--- a/app/models/account_conversation.rb
+++ b/app/models/account_conversation.rb
@@ -101,7 +101,7 @@ class AccountConversation < ApplicationRecord
end
def subscribed_to_timeline?
- Redis.current.exists("subscribed:#{streaming_channel}")
+ Redis.current.exists?("subscribed:#{streaming_channel}")
end
def streaming_channel
diff --git a/app/models/chat_conversation_account.rb b/app/models/chat_conversation_account.rb
index 06c5ec09..629f749b 100644
--- a/app/models/chat_conversation_account.rb
+++ b/app/models/chat_conversation_account.rb
@@ -17,15 +17,26 @@
#
# : todo : expires
+# : todo : max per account
class ChatConversationAccount < ApplicationRecord
include Paginable
+ PER_ACCOUNT_APPROVED_LIMIT = 100
+
+ EXPIRATION_POLICY_MAP = {
+ none: nil,
+ five_minutes: '1',
+ sixty_minutes: '2',
+ six_hours: '3',
+ one_day: '4',
+ three_days: '5',
+ one_week: '6',
+ }.freeze
+
belongs_to :account
belongs_to :chat_conversation
belongs_to :last_chat_message, class_name: 'ChatMessage', optional: true
- # before_validation :set_last_chat_message
-
def participant_accounts
if participant_account_ids.empty?
[account]
@@ -35,10 +46,4 @@ class ChatConversationAccount < ApplicationRecord
end
end
- private
-
- def set_last_chat_message
- self.last_chat_message_id = nil # : todo :
- end
-
end
diff --git a/app/models/home_feed.rb b/app/models/home_feed.rb
index ba756498..845bd1c8 100644
--- a/app/models/home_feed.rb
+++ b/app/models/home_feed.rb
@@ -8,7 +8,7 @@ class HomeFeed < Feed
end
def get(limit, max_id = nil, since_id = nil, min_id = nil)
- if redis.exists("account:#{@account.id}:regeneration")
+ if redis.exists?("account:#{@account.id}:regeneration")
from_database(limit, max_id, since_id, min_id)
else
super
@@ -18,6 +18,7 @@ class HomeFeed < Feed
private
def from_database(limit, max_id, since_id, min_id)
+ puts "tilly from_database"
Status.as_home_timeline(@account)
.paginate_by_id(limit, max_id: max_id, since_id: since_id, min_id: min_id)
.reject { |status| FeedManager.instance.filter?(:home, status, @account.id) }
diff --git a/app/models/link_block.rb b/app/models/link_block.rb
index 6766e1b7..8655276f 100644
--- a/app/models/link_block.rb
+++ b/app/models/link_block.rb
@@ -18,9 +18,13 @@ class LinkBlock < ApplicationRecord
return false if text.nil?
return false if text.length < 1
- urls = text.scan(FetchLinkCardService::URL_PATTERN).map { |array| Addressable::URI.parse(array[0]).normalize }
+ urls = text.scan(FetchLinkCardService::URL_PATTERN).map {|array|
+ Addressable::URI.parse(array[0]).normalize
+ }
url = urls.first
link_for_fetch = TagManager.instance.normalize_link(url)
- where(link: link_for_fetch).exists?
+ link_for_fetch = link_for_fetch.chomp("/")
+
+ where("LOWER(link) LIKE LOWER(?)", "%#{link_for_fetch}%").exists?
end
end
\ No newline at end of file
diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb
index d0fb88c4..48260181 100644
--- a/app/models/media_attachment.rb
+++ b/app/models/media_attachment.rb
@@ -3,22 +3,23 @@
#
# Table name: media_attachments
#
-# id :bigint(8) not null, primary key
-# status_id :bigint(8)
-# file_file_name :string
-# file_content_type :string
-# file_file_size :integer
-# file_updated_at :datetime
-# remote_url :string default(""), not null
-# created_at :datetime not null
-# updated_at :datetime not null
-# shortcode :string
-# type :integer default("image"), not null
-# file_meta :json
-# account_id :bigint(8)
-# description :text
-# scheduled_status_id :bigint(8)
-# blurhash :string
+# id :bigint(8) not null, primary key
+# status_id :bigint(8)
+# file_file_name :string
+# file_content_type :string
+# file_file_size :integer
+# file_updated_at :datetime
+# remote_url :string default(""), not null
+# created_at :datetime not null
+# updated_at :datetime not null
+# shortcode :string
+# type :integer default("image"), not null
+# file_meta :json
+# account_id :bigint(8)
+# description :text
+# scheduled_status_id :bigint(8)
+# blurhash :string
+# media_attachment_album_id :bigint(8)
#
class MediaAttachment < ApplicationRecord
diff --git a/app/models/media_attachment_album.rb b/app/models/media_attachment_album.rb
new file mode 100644
index 00000000..07396fbf
--- /dev/null
+++ b/app/models/media_attachment_album.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+# == Schema Information
+#
+# Table name: media_attachment_albums
+#
+# id :bigint(8) not null, primary key
+# title :text default(""), not null
+# description :text
+# account_id :integer not null
+# visibility :integer default("public"), not null
+# created_at :datetime not null
+# updated_at :datetime not null
+# cover_id :bigint(8)
+#
+
+class MediaAttachmentAlbum < ApplicationRecord
+
+ enum visibility: [
+ :public,
+ :private,
+ ], _suffix: :visibility
+
+ belongs_to :account
+
+end
diff --git a/app/models/status.rb b/app/models/status.rb
index 40473320..57b444c6 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -290,13 +290,13 @@ class Status < ApplicationRecord
end
def as_home_timeline(account)
- query = where('created_at > ?', 5.days.ago)
+ query = where('created_at > ?', 10.days.ago)
query.where(visibility: [:public, :unlisted, :private])
query.where(account: [account] + account.following).without_replies
end
def as_group_timeline(group)
- query = where('created_at > ?', 5.days.ago)
+ query = where('created_at > ?', 10.days.ago)
query.where(group: group).without_replies
end
diff --git a/app/models/status_bookmark.rb b/app/models/status_bookmark.rb
index ecdc01d0..4d063a4b 100644
--- a/app/models/status_bookmark.rb
+++ b/app/models/status_bookmark.rb
@@ -3,11 +3,12 @@
#
# Table name: status_bookmarks
#
-# id :bigint(8) not null, primary key
-# created_at :datetime not null
-# updated_at :datetime not null
-# account_id :bigint(8) not null
-# status_id :bigint(8) not null
+# id :bigint(8) not null, primary key
+# created_at :datetime not null
+# updated_at :datetime not null
+# account_id :bigint(8) not null
+# status_id :bigint(8) not null
+# status_bookmark_collection_id :bigint(8)
#
class StatusBookmark < ApplicationRecord
diff --git a/app/models/status_bookmark_collection.rb b/app/models/status_bookmark_collection.rb
new file mode 100644
index 00000000..5d509fbf
--- /dev/null
+++ b/app/models/status_bookmark_collection.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+# == Schema Information
+#
+# Table name: status_bookmark_collections
+#
+# id :bigint(8) not null, primary key
+# title :text default(""), not null
+# account_id :integer not null
+# created_at :datetime not null
+# updated_at :datetime not null
+#
+
+class StatusBookmarkCollection < ApplicationRecord
+
+ PER_ACCOUNT_LIMIT = 100
+
+ belongs_to :account
+
+end
diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb
index 4afb8d77..2ee79702 100644
--- a/app/serializers/initial_state_serializer.rb
+++ b/app/serializers/initial_state_serializer.rb
@@ -31,6 +31,7 @@ class InitialStateSerializer < ActiveModel::Serializer
store[:unread_count] = unread_count object.current_account
store[:last_read_notification_id] = object.current_account.user.last_read_notification
store[:monthly_expenses_complete] = Redis.current.get("monthly_funding_amount") || 0
+ store[:trending_hashtags] = get_trending_hashtags
store[:is_first_session] = is_first_session object.current_account
store[:email_confirmed] = object.current_account.user.confirmed?
store[:email] = object.current_account.user.confirmed? ? '[hidden]' : object.current_account.user.email
@@ -39,6 +40,7 @@ class InitialStateSerializer < ActiveModel::Serializer
store
end
+
def compose
store = {}
@@ -78,4 +80,9 @@ class InitialStateSerializer < ActiveModel::Serializer
object.current_account.user.sign_in_count === 1
end
+ def get_trending_hashtags
+ tags = Redis.current.get("admin_trending_hashtags") || ""
+ return tags.strip.split(", ")
+ end
+
end
diff --git a/app/serializers/rest/account_serializer.rb b/app/serializers/rest/account_serializer.rb
index 640538d3..fc629c7e 100644
--- a/app/serializers/rest/account_serializer.rb
+++ b/app/serializers/rest/account_serializer.rb
@@ -4,7 +4,7 @@ class REST::AccountSerializer < ActiveModel::Serializer
include RoutingHelper
attributes :id, :username, :acct, :display_name, :locked, :bot, :created_at,
- :note, :url, :avatar, :avatar_static, :header, :header_static,
+ :note, :url, :avatar, :avatar_static, :header, :header_static, :is_flagged_as_spam,
:followers_count, :following_count, :statuses_count, :is_pro, :is_verified, :is_donor, :is_investor
has_one :moved_to_account, key: :moved, serializer: REST::AccountSerializer, if: :moved_and_not_nested?
diff --git a/app/serializers/rest/chat_conversation_account_serializer.rb b/app/serializers/rest/chat_conversation_account_serializer.rb
index 4edd52cd..f1ea1b42 100644
--- a/app/serializers/rest/chat_conversation_account_serializer.rb
+++ b/app/serializers/rest/chat_conversation_account_serializer.rb
@@ -1,7 +1,9 @@
# frozen_string_literal: true
class REST::ChatConversationAccountSerializer < ActiveModel::Serializer
- attributes :id, :is_hidden, :is_approved, :unread_count, :is_unread, :chat_conversation_id, :created_at
+ attributes :id, :is_hidden, :is_approved, :unread_count,
+ :is_unread, :chat_conversation_id, :created_at,
+ :is_blocked, :is_muted, :chat_message_expiration_policy
has_many :participant_accounts, key: :other_accounts, serializer: REST::AccountSerializer
has_one :last_chat_message, serializer: REST::ChatMessageSerializer, unless: :last_chat_message_id?
@@ -22,4 +24,12 @@ class REST::ChatConversationAccountSerializer < ActiveModel::Serializer
object.unread_count > 0
end
+ def is_blocked
+ false
+ end
+
+ def is_muted
+ false
+ end
+
end
diff --git a/app/serializers/rest/chat_message_serializer.rb b/app/serializers/rest/chat_message_serializer.rb
index 813177cf..c619a298 100644
--- a/app/serializers/rest/chat_message_serializer.rb
+++ b/app/serializers/rest/chat_message_serializer.rb
@@ -2,7 +2,7 @@
class REST::ChatMessageSerializer < ActiveModel::Serializer
attributes :id, :text_html, :text, :language, :from_account_id,
- :chat_conversation_id, :created_at
+ :chat_conversation_id, :created_at, :expires_at
def id
object.id.to_s
diff --git a/app/services/delete_chat_message_service.rb b/app/services/delete_chat_message_service.rb
new file mode 100644
index 00000000..3b7f40f6
--- /dev/null
+++ b/app/services/delete_chat_message_service.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class DeleteChatMessageService < BaseService
+ def call(account, chatMessageId)
+ @chat = ChatMessage.where(from_account: account).find(chatMessageId)
+
+ # : todo :
+ # make sure last_chat_message_id in chat_account_conversation gets set to last
+
+ @chat.destroy!
+ end
+end
diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb
index f224f6a8..bb9339fb 100644
--- a/app/services/fan_out_on_write_service.rb
+++ b/app/services/fan_out_on_write_service.rb
@@ -6,12 +6,7 @@ class FanOutOnWriteService < BaseService
# @param [Status] status
def call(status)
raise GabSocial::RaceConditionError if status.visibility.nil?
-
- if status.direct_visibility? || status.limited_visibility?
- #
- else
- deliver_to_self(status) if status.account.local?
- end
+ deliver_to_self(status) if status.account.local?
end
private
diff --git a/app/services/post_chat_message_service.rb b/app/services/post_chat_message_service.rb
new file mode 100644
index 00000000..212b52e8
--- /dev/null
+++ b/app/services/post_chat_message_service.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+class PostChatMessageService < BaseService
+
+ def call(account, options = {})
+ @account = account
+ @options = options
+ @text = @options[:text] || ''
+ @chat_conversation = @options[:chat_conversation]
+
+ preprocess_attributes!
+
+ validate_text!
+ validate_links!
+
+ set_chat_conversation_recipients!
+ set_message_expiration_date!
+
+ process_chat!
+ postprocess_chat!
+
+ @chat
+ end
+
+ def preprocess_attributes!
+ @text = ActionController::Base.helpers.strip_tags(@text)
+ unless @chat_conversation
+ raise ActiveRecord::RecordInvalid
+ end
+ rescue ArgumentError
+ raise ActiveRecord::RecordInvalid
+ end
+
+ def validate_links!
+ raise GabSocial::NotPermittedError if LinkBlock.block?(@text)
+ end
+
+ def validate_text!
+ raise GabSocial::NotPermittedError if @text.nil? || @text.strip.length == 0
+ end
+
+ def process_chat!
+ @chat = ChatMessage.create!(
+ from_account: @account,
+ chat_conversation: @chat_conversation,
+ text: @text
+ expires_at: @expires_at
+ )
+ end
+
+ def postprocess_chat!
+ @chat_conversation_recipients_accounts = ChatConversationAccount.where(chat_conversation: @chat_conversation)
+ @chat_conversation_recipients_accounts.each do |recipient|
+ recipient.last_chat_message_id = @chat.id
+ recipient.is_hidden = false # reset to show unless blocked
+
+ # Get not mine
+ if @account_conversation.id != recipient.id
+ recipient.unread_count = recipient.unread_count + 1
+
+ # : todo :
+ # check if muting, redis
+ payload = InlineRenderer.render(@chat, recipient.account, :chat_message)
+ Redis.current.publish("chat_messages:#{recipient.account.id}", Oj.dump(event: :notification, payload: payload))
+ else
+ recipient.unread_count = 0
+ end
+
+ recipient.save
+ end
+ end
+
+ def set_chat_conversation_recipients!
+ # : todo :
+ # check if chat blocked
+ # check if normal blocked
+
+ @account_conversation = ChatConversationAccount.where(account: @account, chat_conversation: @chat_conversation).first
+ rescue ArgumentError
+ raise ActiveRecord::RecordInvalid
+ end
+
+ def set_message_expiration_date
+ case @account_conversation.expiration_policy
+ when :five_minutes
+ @expires_at = 5.minutes
+ when :sixty_minutes
+ @expires_at = 1.hour
+ when :six_hours
+ @expires_at = 6.hours
+ when :one_day
+ @expires_at = 1.day
+ when :three_days
+ @expires_at = 3.days
+ when :one_week
+ @expires_at = 1.week
+ else
+ @expires_at = nil
+ end
+
+ @expires_at
+ end
+
+end
diff --git a/app/services/purge_chat_messages_service.rb b/app/services/purge_chat_messages_service.rb
new file mode 100644
index 00000000..b7bd5886
--- /dev/null
+++ b/app/services/purge_chat_messages_service.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+class PurgeChatMessagesService < BaseService
+ def call(account, chat_conversation)
+ unless account.is_pro
+ raise GabSocial::NotPermittedError
+ end
+
+ # Destroy all
+ ChatMessage.where(from_account: account, chat_conversation: chat_conversation).in_batches.destroy_all
+
+ @last_chat_in_conversation = ChatMessage.where(chat_conversation: chat_conversation).first
+
+ @chat_conversation_recipients_accounts = ChatConversationAccount.where(chat_conversation: chat_conversation)
+ @chat_conversation_recipients_accounts.each do |recipient|
+ # make sure last_chat_message_id in chat_account_conversation gets set to last
+ unless @last_chat_in_conversation.nil?
+ recipient.last_chat_message_id = @last_chat_in_conversation.id
+ else
+ recipient.last_chat_message_id = nil
+ end
+
+ # Reset and save
+ recipient.unread_count = 0
+ recipient.save
+ end
+ end
+end
diff --git a/app/services/suspend_account_service.rb b/app/services/suspend_account_service.rb
index 22db5eb6..702e1b7f 100644
--- a/app/services/suspend_account_service.rb
+++ b/app/services/suspend_account_service.rb
@@ -5,14 +5,20 @@ class SuspendAccountService < BaseService
active_relationships
block_relationships
blocked_by_relationships
+ chat_block_relationships
+ chat_blocked_by_relationships
conversations
+ chat_conversations
+ chat_messages
custom_filters
favourites
follow_requests
list_accounts
media_attachments
mute_relationships
+ chat_mute_relationships
muted_by_relationships
+ chat_muted_by_relationships
notifications
owned_lists
passive_relationships
@@ -21,6 +27,10 @@ class SuspendAccountService < BaseService
status_bookmarks
status_pins
subscriptions
+ group_accounts
+ group_join_requests
+ group_removed_accounts
+ shortcuts
).freeze
ASSOCIATIONS_ON_DESTROY = %w(
diff --git a/app/services/update_account_service.rb b/app/services/update_account_service.rb
index 66ee435a..d5165065 100644
--- a/app/services/update_account_service.rb
+++ b/app/services/update_account_service.rb
@@ -5,6 +5,10 @@ class UpdateAccountService < BaseService
was_locked = account.locked
update_method = raise_error ? :update! : :update
+ # : todo :
+ # check if link blocking
+ # set account.is_flagged_as_spam
+
account.send(update_method, params).tap do |ret|
next unless ret
diff --git a/app/views/accounts/_bio.html.haml b/app/views/accounts/_bio.html.haml
index efc26d13..95b5b545 100644
--- a/app/views/accounts/_bio.html.haml
+++ b/app/views/accounts/_bio.html.haml
@@ -20,8 +20,6 @@
= fa_icon 'check'
= Formatter.instance.format_field(account, field.value, custom_emojify: true)
- = account_badge(account)
-
- if account.note.present?
.account__header__content.emojify= Formatter.instance.simplified_format(account, custom_emojify: true)
diff --git a/app/views/admin/accounts/_account.html.haml b/app/views/admin/accounts/_account.html.haml
index 6fe9863f..6e26a141 100644
--- a/app/views/admin/accounts/_account.html.haml
+++ b/app/views/admin/accounts/_account.html.haml
@@ -1,8 +1,6 @@
%tr
%td
= admin_account_link_to(account)
- %td
- %div{ style: 'margin: -2px 0' }= account_badge(account, all: true)
%td
- if account.user_current_sign_in_ip
%samp.ellipsized-ip{ title: account.user_current_sign_in_ip }= account.user_current_sign_in_ip
@@ -12,4 +10,6 @@
- if account.user_current_sign_in_at
%time.time-ago{ datetime: account.user_current_sign_in_at.iso8601, title: l(account.user_current_sign_in_at) }= l account.user_current_sign_in_at
- else
- \-
\ No newline at end of file
+ \-
+ %td
+ %samp= number_with_delimiter account.statuses.count
\ No newline at end of file
diff --git a/app/views/admin/accounts/index.html.haml b/app/views/admin/accounts/index.html.haml
index 5d4d8715..c99a2116 100644
--- a/app/views/admin/accounts/index.html.haml
+++ b/app/views/admin/accounts/index.html.haml
@@ -34,9 +34,9 @@
%thead
%tr
%th= t('admin.accounts.username')
- %th= t('admin.accounts.role')
%th= t('admin.accounts.most_recent_ip')
%th= t('admin.accounts.most_recent_activity')
+ %th Status count
%th
%tbody
= render @accounts
diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml
index b1b604f9..2302fa9f 100644
--- a/app/views/admin/accounts/show.html.haml
+++ b/app/views/admin/accounts/show.html.haml
@@ -42,6 +42,18 @@
- else
%span.neutral= t('admin.accounts.no_limits_imposed')
.dashboard__counters__label= t 'admin.accounts.login_status'
+ %div
+ = link_to admin_account_joined_groups_path(@account.id) do
+ .dashboard__counters__num= number_with_delimiter @account.groups.count
+ .dashboard__counters__label Joined Groups
+ %div
+ = link_to admin_account_chat_conversations_path(@account.id) do
+ .dashboard__counters__num= number_with_delimiter @account.chat_conversation_accounts_count
+ .dashboard__counters__label Chat Conversations
+ %div
+ = link_to admin_account_chat_messages_path(@account.id) do
+ .dashboard__counters__num= number_with_delimiter @account.chat_messages_count
+ .dashboard__counters__label Chat Messages
- unless @account.local? && @account.user.nil?
.table-wrapper
diff --git a/app/views/admin/chat_conversations/index.html.haml b/app/views/admin/chat_conversations/index.html.haml
new file mode 100644
index 00000000..25f1f290
--- /dev/null
+++ b/app/views/admin/chat_conversations/index.html.haml
@@ -0,0 +1,28 @@
+- content_for :page_title do
+ = t('admin.followers.title', acct: @account.acct)
+
+.filters
+ .filter-subset
+ %strong= t('admin.accounts.location.title')
+ %ul
+ %li= link_to t('admin.accounts.location.local'), admin_account_followers_path(@account.id), class: 'selected'
+ .back-link{ style: 'flex: 1 1 auto; text-align: right' }
+ = link_to admin_account_path(@account.id) do
+ = fa_icon 'chevron-left fw'
+ = t('admin.followers.back_to_account')
+
+%hr.spacer/
+
+.table-wrapper
+ %table.table
+ %thead
+ %tr
+ %th= t('admin.accounts.username')
+ %th= t('admin.accounts.role')
+ %th= t('admin.accounts.most_recent_ip')
+ %th= t('admin.accounts.most_recent_activity')
+ %th
+ %tbody
+ = render partial: 'admin/accounts/account', collection: @followers
+
+= paginate @followers
diff --git a/app/views/admin/chat_messages/index.html.haml b/app/views/admin/chat_messages/index.html.haml
new file mode 100644
index 00000000..25f1f290
--- /dev/null
+++ b/app/views/admin/chat_messages/index.html.haml
@@ -0,0 +1,28 @@
+- content_for :page_title do
+ = t('admin.followers.title', acct: @account.acct)
+
+.filters
+ .filter-subset
+ %strong= t('admin.accounts.location.title')
+ %ul
+ %li= link_to t('admin.accounts.location.local'), admin_account_followers_path(@account.id), class: 'selected'
+ .back-link{ style: 'flex: 1 1 auto; text-align: right' }
+ = link_to admin_account_path(@account.id) do
+ = fa_icon 'chevron-left fw'
+ = t('admin.followers.back_to_account')
+
+%hr.spacer/
+
+.table-wrapper
+ %table.table
+ %thead
+ %tr
+ %th= t('admin.accounts.username')
+ %th= t('admin.accounts.role')
+ %th= t('admin.accounts.most_recent_ip')
+ %th= t('admin.accounts.most_recent_activity')
+ %th
+ %tbody
+ = render partial: 'admin/accounts/account', collection: @followers
+
+= paginate @followers
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 44ce4235..19cc1fd0 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -26,6 +26,18 @@
= link_to sidekiq_url do
.dashboard__counters__num= number_with_delimiter @queue_backlog
.dashboard__counters__label= t 'admin.dashboard.backlog'
+ %div
+ %div
+ .dashboard__counters__num= number_with_delimiter @statuses_count
+ .dashboard__counters__label Status count
+ %div
+ %div
+ .dashboard__counters__num= number_with_delimiter @pro_accounts_count
+ .dashboard__counters__label PRO Users
+ %div
+ %div
+ .dashboard__counters__num= number_with_delimiter @donor_accounts_count
+ .dashboard__counters__label Donor Users
.dashboard__widgets
.dashboard__widgets__users
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
new file mode 100644
index 00000000..9b844e2d
--- /dev/null
+++ b/app/views/admin/groups/show.html.haml
@@ -0,0 +1,35 @@
+- content_for :page_title do
+ = @group.title
+
+-# : todo :
+-# add/remove admin
+-# add/remove moderator
+-# feature/unfeature
+-# delete
+-# view accounts
+-# view removed accounts
+-# number of accounts
+-# number of removed accounts
+-# number of posts
+
+.card.h-card{:style => "height:300px"}
+ .card__img
+ = image_tag @group.cover_image.url, alt: '', :style => "height:300px"
+
+.dashboard__counters{ style: 'margin-top: 10px' }
+ %div
+ %div
+ .dashboard__counters__num= number_with_delimiter 0 #@account.statuses_count
+ .dashboard__counters__label Status Count
+ %div
+ %div
+ .dashboard__counters__num= number_to_human_size 0 #@account.media_attachments.sum('file_file_size')
+ .dashboard__counters__label Member Count
+ %div
+ %div
+ .dashboard__counters__num= number_with_delimiter 0 #@account.local_followers_count
+ .dashboard__counters__label Removed Members Count
+ %div
+ %div
+ .dashboard__counters__num= number_with_delimiter 0 #@account.reports.count
+ .dashboard__counters__label Member Requests Count
\ No newline at end of file
diff --git a/app/views/admin/joined_groups/index.html.haml b/app/views/admin/joined_groups/index.html.haml
new file mode 100644
index 00000000..51e6d4c1
--- /dev/null
+++ b/app/views/admin/joined_groups/index.html.haml
@@ -0,0 +1,15 @@
+- content_for :page_title do
+ = "@#{@account.username} - Joined Groups"
+
+.table-wrapper
+ %table.table
+ %thead
+ %tr
+ %th= t('admin.groups.id')
+ %th= t('admin.groups.title')
+ %th= t('admin.groups.member_count')
+ %th
+ %tbody
+ = render @groups
+
+= paginate @groups
\ No newline at end of file
diff --git a/app/views/auth/registrations/new.html.haml b/app/views/auth/registrations/new.html.haml
index 54842686..15e107f1 100644
--- a/app/views/auth/registrations/new.html.haml
+++ b/app/views/auth/registrations/new.html.haml
@@ -23,7 +23,7 @@
= f.input :password_confirmation, wrapper: :with_label, label: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'off' }
.fields-group
- = f.input :challenge, wrapper: :with_label, label: "Are you human? What is #{@challenge_add_1} + #{@challenge_add_2} = ", required: true, input_html: { 'aria-label' => "Are you human? What is #{@challenge_add_1} + #{@challenge_add_2}", :autocomplete => 'off' }
+ = f.input :challenge, wrapper: :with_label, label: "Are you a human? What is #{@challenge_add_1} + #{@challenge_add_2} = ", required: true, input_html: { 'aria-label' => "Are you a human? What is #{@challenge_add_1} + #{@challenge_add_2}", :autocomplete => 'off' }
.fields-group-agreement
= f.input :agreement, as: :boolean, wrapper: :with_label, label: t('auth.checkbox_agreement_html', about_tos_path: about_tos_path)
diff --git a/app/views/settings/profiles/show.html.haml b/app/views/settings/profiles/show.html.haml
index c17767f7..6506a2da 100644
--- a/app/views/settings/profiles/show.html.haml
+++ b/app/views/settings/profiles/show.html.haml
@@ -7,12 +7,17 @@
.fields-row
.fields-row__column.fields-group.fields-row__column-6
- if @account.is_verified
- %span Verified accounts cannot change their display name
+ %div
+ %span Verified accounts cannot change their username or display name. Please contact
+ %a{:href => "https://gab.com/help", :style => "color:#3ACD80;text-decoration:none;outline:0;", :target => "_blank"} @help
+ %span for more information.
%br
%br
+ - elsif @account.is_pro
+ = f.input :username, wrapper: :with_label, input_html: { maxlength: 30 }, hint: false
- else
= f.input :display_name, wrapper: :with_label, input_html: { maxlength: 30 }, hint: false
- = f.input :note, wrapper: :with_label, input_html: { maxlength: 500 }, hint: false
+ = f.input :note, wrapper: :with_label, input_html: { maxlength: 500 }, hint: false
.fields-row
.fields-row__column.fields-row__column-6
diff --git a/app/views/settings/trending_hashtags/index.html.haml b/app/views/settings/trending_hashtags/index.html.haml
new file mode 100644
index 00000000..352d228c
--- /dev/null
+++ b/app/views/settings/trending_hashtags/index.html.haml
@@ -0,0 +1,6 @@
+- content_for :page_title do
+ = 'Trending Hashtags'
+
+= form_tag settings_trending_hashtags_url, :method => :post do
+ = text_field_tag "trending_hashtags", "", :style => 'width:400px', placeholder: "StopTheSteal, Trump2020, Election2020", :value => @trending_hashtags
+ = submit_tag "Submit"
diff --git a/app/views/settings/verifications/moderation/_verification_request.html.haml b/app/views/settings/verifications/moderation/_verification_request.html.haml
index 4c17dcb8..6fb10a61 100644
--- a/app/views/settings/verifications/moderation/_verification_request.html.haml
+++ b/app/views/settings/verifications/moderation/_verification_request.html.haml
@@ -6,3 +6,4 @@
%td
= table_link_to 'download', t('verifications.moderation.view_proof'), verification_request.image.url, target: "_blank"
= table_link_to 'checkmark', t('verifications.moderation.approve'), settings_verifications_approve_url(verification_request.id)
+ = table_link_to 'checkmark', t('verifications.moderation.reject'), settings_verifications_reject_url(verification_request.id)
diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb
index a2628b68..8ab1916a 100644
--- a/config/initializers/rack_attack.rb
+++ b/config/initializers/rack_attack.rb
@@ -87,17 +87,27 @@ class Rack::Attack
API_DELETE_REBLOG_REGEX = /\A\/api\/v1\/statuses\/[\d]+\/unreblog/.freeze
API_DELETE_STATUS_REGEX = /\A\/api\/v1\/statuses\/[\d]+/.freeze
+ API_POST_CHAT_MESSAGE_REGEX = /\A\/api\/v1\/chat_messages/.freeze
+ API_POST_FOLLOW_REGEX = /\A\/api\/v1\/accounts\/[\d]+\/follow/.freeze
API_POST_GROUP_PASSWORD_CHECK_REGEX = /\A\/api\/v1\/groups\/[\d]+\/password/.freeze
throttle('throttle_api_delete', limit: 30, period: 30.minutes) do |req|
req.authenticated_user_id if (req.post? && req.path =~ API_DELETE_REBLOG_REGEX) || (req.delete? && req.path =~ API_DELETE_STATUS_REGEX)
end
+ throttle('throttle_api_chat_message', limit: 1000, period: 1.day) do |req|
+ req.authenticated_user_id if req.post? && req.path =~ API_POST_CHAT_MESSAGE_REGEX
+ end
+
+ throttle('throttle_api_follow', limit: 200, period: 1.day) do |req|
+ req.authenticated_user_id if req.post? && req.path =~ API_POST_FOLLOW_REGEX
+ end
+
throttle('throttle_group_password_check', limit: 5, period: 1.minute) do |req|
req.authenticated_user_id if req.post? && req.path =~ API_POST_GROUP_PASSWORD_CHECK_REGEX
end
- throttle('protected_paths', limit: 25, period: 5.minutes) do |req|
+ throttle('protected_paths', limit: 10, period: 5.minutes) do |req|
req.remote_ip if req.post? && req.path =~ PROTECTED_PATHS_REGEX
end
diff --git a/config/locales/en.yml b/config/locales/en.yml
index d217b89b..0e527f42 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -1151,6 +1151,7 @@ en:
approve: Approve
approved_msg: Account has been verified.
reject: Reject
+ rejected_msg: Successfully rejected.
billing:
upgrade:
explanation_html: Here you can upgrade to PRO.
diff --git a/config/navigation.rb b/config/navigation.rb
index 6881b791..f2ed39f5 100644
--- a/config/navigation.rb
+++ b/config/navigation.rb
@@ -45,6 +45,7 @@ SimpleNavigation::Configuration.run do |navigation|
s.item :promotions, safe_join([fa_icon('star fw'), t('promotions.title')]), settings_promotions_url, if: -> { current_user.admin? }
s.item :monthly_funding, safe_join([fa_icon('money fw'), t('monthly_funding.title')]), settings_expenses_url, if: -> { current_user.admin? }
s.item :group_categories, safe_join([fa_icon('users fw'), t('group_categories.title')]), settings_group_categories_url, if: -> { current_user.admin? }
+ s.item :trending_hashtags, safe_join([fa_icon('hashtag fw'), 'Trending Hashtags']), settings_trending_hashtags_url, if: -> { current_user.admin? }
end
n.item :logout, safe_join([fa_icon('sign-out fw'), t('auth.logout')]), destroy_user_session_url, link_html: { 'data-method' => 'delete' }
diff --git a/config/routes.rb b/config/routes.rb
index f129f650..ca2019e5 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -53,6 +53,7 @@ Rails.application.routes.draw do
resources :promotions, only: [:index, :new, :create, :edit, :update, :destroy]
resources :expenses, only: [:index, :new, :create, :edit, :update, :destroy]
resources :group_categories, only: [:index, :new, :create, :edit, :update, :destroy]
+ resources :trending_hashtags, only: [:index, :new, :create, :edit, :update, :destroy]
namespace :verifications do
get :moderation, to: 'moderation#index', as: :moderation
@@ -144,6 +145,9 @@ Rails.application.routes.draw do
resource :action, only: [:new, :create], controller: 'account_actions'
resources :statuses, only: [:index, :show, :create, :update, :destroy]
resources :followers, only: [:index]
+ resources :joined_groups, only: [:index]
+ resources :chat_conversations, only: [:index]
+ resources :chat_messages, only: [:index, :show, :create, :update, :destroy]
resource :confirmation, only: [:create] do
collection do
@@ -165,7 +169,7 @@ Rails.application.routes.draw do
resources :custom_emojis, only: [:index, :new, :create, :update, :destroy]
- resources :groups, only: [:index, :destroy] do
+ resources :groups, only: [:index, :show, :update, :destroy] do
member do
post :enable_featured
post :disable_featured
@@ -233,6 +237,7 @@ Rails.application.routes.draw do
post :unblock_messenger
post :mute_messenger
post :unmute_messenger
+ post :set_expiration_policy
end
end
@@ -257,24 +262,20 @@ Rails.application.routes.draw do
resources :chat_conversation, only: [:show, :create] do
member do
post :mark_chat_conversation_approved
- post :mark_chat_conversation_unread
+ post :mark_chat_conversation_read
post :mark_chat_conversation_hidden
end
end
- resources :links, only: :show
- resource :popular_links, only: :show
- resources :streaming, only: [:index]
+ resources :links, only: :show
+ resource :popular_links, only: :show
+ resources :streaming, only: [:index]
resources :custom_emojis, only: [:index]
- resources :suggestions, only: [:index, :destroy]
+ resources :suggestions, only: [:index, :destroy]
resources :scheduled_statuses, only: [:index, :show, :update, :destroy]
- resources :preferences, only: [:index]
+ resources :preferences, only: [:index]
resources :group_categories, only: [:index]
resources :chat_messages, only: [:create, :destroy]
-
- get '/search', to: 'search#index', as: :search
- get '/account_by_username/:username', to: 'account_by_username#show', username: username_regex
-
resources :promotions, only: [:index]
resources :follows, only: [:create]
resources :media, only: [:create, :update]
@@ -285,6 +286,11 @@ Rails.application.routes.draw do
resources :filters, only: [:index, :create, :show, :update, :destroy]
resources :shortcuts, only: [:index, :create, :show, :destroy]
resources :bookmarks, only: [:index]
+ resources :bookmark_collections, only: [:index, :create, :update, :show, :destroy]
+ resources :albums, only: [:index, :create, :update, :show, :destroy]
+
+ get '/search', to: 'search#index', as: :search
+ get '/account_by_username/:username', to: 'account_by_username#show', username: username_regex
namespace :apps do
get :verify_credentials, to: 'credentials#show'
diff --git a/config/settings.yml b/config/settings.yml
index 8dd4b074..d458d876 100644
--- a/config/settings.yml
+++ b/config/settings.yml
@@ -22,6 +22,11 @@ defaults: &defaults
noindex: false
theme: 'default'
aggregate_reblogs: true
+ chat_messages:
+ restrict_non_followers: true
+ show_active: false
+ read_receipts: false
+ sounds: true
notification_emails:
follow: false
reblog: false
diff --git a/db/migrate/20201212154128_add_is_flagged_as_spam_to_accounts.rb b/db/migrate/20201212154128_add_is_flagged_as_spam_to_accounts.rb
new file mode 100644
index 00000000..fd147104
--- /dev/null
+++ b/db/migrate/20201212154128_add_is_flagged_as_spam_to_accounts.rb
@@ -0,0 +1,9 @@
+class AddIsFlaggedAsSpamToAccounts < ActiveRecord::Migration[5.2]
+ def up
+ safety_assured { add_column :accounts, :is_flagged_as_spam, :bool, default: false, null: false }
+ end
+
+ def down
+ remove_column :accounts, :is_pro
+ end
+end
diff --git a/db/migrate/20201214051317_create_status_bookmark_collections.rb b/db/migrate/20201214051317_create_status_bookmark_collections.rb
new file mode 100644
index 00000000..857860ce
--- /dev/null
+++ b/db/migrate/20201214051317_create_status_bookmark_collections.rb
@@ -0,0 +1,10 @@
+class CreateStatusBookmarkCollections < ActiveRecord::Migration[5.2]
+ def change
+ create_table :status_bookmark_collections do |t|
+ t.text :title, null: false, default: ''
+ t.integer :account_id, null: false
+
+ t.timestamps null: false
+ end
+ end
+end
diff --git a/db/migrate/20201214051720_create_media_attachment_albums.rb b/db/migrate/20201214051720_create_media_attachment_albums.rb
new file mode 100644
index 00000000..a90bf8b9
--- /dev/null
+++ b/db/migrate/20201214051720_create_media_attachment_albums.rb
@@ -0,0 +1,12 @@
+class CreateMediaAttachmentAlbums < ActiveRecord::Migration[5.2]
+ def change
+ create_table :media_attachment_albums do |t|
+ t.text :title, null: false, default: ''
+ t.text :description
+ t.integer :account_id, null: false
+ t.integer :visibility, null: false, default: 0
+
+ t.timestamps null: false
+ end
+ end
+end
\ No newline at end of file
diff --git a/db/migrate/20201214054428_add_status_bookmark_collection_id_to_status_bookmarks.rb b/db/migrate/20201214054428_add_status_bookmark_collection_id_to_status_bookmarks.rb
new file mode 100644
index 00000000..685edda1
--- /dev/null
+++ b/db/migrate/20201214054428_add_status_bookmark_collection_id_to_status_bookmarks.rb
@@ -0,0 +1,7 @@
+class AddStatusBookmarkCollectionIdToStatusBookmarks < ActiveRecord::Migration[5.2]
+ def change
+ safety_assured {
+ add_reference :status_bookmarks, :status_bookmark_collection, foreign_key: { on_delete: :nullify }
+ }
+ end
+end
diff --git a/db/migrate/20201214055039_add_album_id_to_media_attachments.rb b/db/migrate/20201214055039_add_album_id_to_media_attachments.rb
new file mode 100644
index 00000000..7366b41d
--- /dev/null
+++ b/db/migrate/20201214055039_add_album_id_to_media_attachments.rb
@@ -0,0 +1,7 @@
+class AddAlbumIdToMediaAttachments < ActiveRecord::Migration[5.2]
+ def change
+ safety_assured {
+ add_reference :media_attachments, :media_attachment_album, foreign_key: { on_delete: :nullify }
+ }
+ end
+end
diff --git a/db/migrate/20201215203113_add_cover_id_to_media_attachment_albums.rb b/db/migrate/20201215203113_add_cover_id_to_media_attachment_albums.rb
new file mode 100644
index 00000000..90cf36d3
--- /dev/null
+++ b/db/migrate/20201215203113_add_cover_id_to_media_attachment_albums.rb
@@ -0,0 +1,7 @@
+class AddCoverIdToMediaAttachmentAlbums < ActiveRecord::Migration[5.2]
+ disable_ddl_transaction!
+
+ def change
+ add_reference :media_attachment_albums, :cover, null: true, default: nil, index: { algorithm: :concurrently }
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 5414eac9..ca75cd76 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2020_12_06_060226) do
+ActiveRecord::Schema.define(version: 2020_12_15_203113) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_stat_statements"
@@ -134,6 +134,7 @@ ActiveRecord::Schema.define(version: 2020_12_06_060226) do
t.boolean "is_verified", default: false, null: false
t.boolean "is_donor", default: false, null: false
t.boolean "is_investor", default: false, null: false
+ t.boolean "is_flagged_as_spam", default: false, null: false
t.index "(((setweight(to_tsvector('simple'::regconfig, (display_name)::text), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, (username)::text), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(domain, ''::character varying))::text), 'C'::\"char\")))", name: "search_index", using: :gin
t.index "lower((username)::text), lower((domain)::text)", name: "index_accounts_on_username_and_domain_lower", unique: true
t.index ["is_donor"], name: "index_accounts_on_is_donor"
@@ -418,6 +419,17 @@ ActiveRecord::Schema.define(version: 2020_12_06_060226) do
t.index ["account_id"], name: "index_lists_on_account_id"
end
+ create_table "media_attachment_albums", force: :cascade do |t|
+ t.text "title", default: "", null: false
+ t.text "description"
+ t.integer "account_id", null: false
+ t.integer "visibility", default: 0, null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.bigint "cover_id"
+ t.index ["cover_id"], name: "index_media_attachment_albums_on_cover_id"
+ end
+
create_table "media_attachments", force: :cascade do |t|
t.bigint "status_id"
t.string "file_file_name"
@@ -434,7 +446,9 @@ ActiveRecord::Schema.define(version: 2020_12_06_060226) do
t.text "description"
t.bigint "scheduled_status_id"
t.string "blurhash"
+ t.bigint "media_attachment_album_id"
t.index ["account_id"], name: "index_media_attachments_on_account_id"
+ t.index ["media_attachment_album_id"], name: "index_media_attachments_on_media_attachment_album_id"
t.index ["scheduled_status_id"], name: "index_media_attachments_on_scheduled_status_id"
t.index ["shortcode"], name: "index_media_attachments_on_shortcode", unique: true
t.index ["status_id"], name: "index_media_attachments_on_status_id"
@@ -671,13 +685,22 @@ ActiveRecord::Schema.define(version: 2020_12_06_060226) do
t.index ["var"], name: "index_site_uploads_on_var", unique: true
end
+ create_table "status_bookmark_collections", force: :cascade do |t|
+ t.text "title", default: "", null: false
+ t.integer "account_id", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
create_table "status_bookmarks", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.bigint "account_id", null: false
t.bigint "status_id", null: false
+ t.bigint "status_bookmark_collection_id"
t.index ["account_id", "status_id"], name: "index_status_bookmarks_on_account_id_and_status_id", unique: true
t.index ["account_id"], name: "index_status_bookmarks_on_account_id"
+ t.index ["status_bookmark_collection_id"], name: "index_status_bookmarks_on_status_bookmark_collection_id"
t.index ["status_id"], name: "index_status_bookmarks_on_status_id"
end
@@ -887,6 +910,7 @@ ActiveRecord::Schema.define(version: 2020_12_06_060226) do
add_foreign_key "list_accounts", "lists", on_delete: :cascade
add_foreign_key "lists", "accounts", on_delete: :cascade
add_foreign_key "media_attachments", "accounts", name: "fk_96dd81e81b", on_delete: :nullify
+ add_foreign_key "media_attachments", "media_attachment_albums", on_delete: :nullify
add_foreign_key "media_attachments", "scheduled_statuses", on_delete: :nullify
add_foreign_key "media_attachments", "statuses", on_delete: :nullify
add_foreign_key "mentions", "accounts", name: "fk_970d43f9d1", on_delete: :cascade
@@ -915,6 +939,7 @@ ActiveRecord::Schema.define(version: 2020_12_06_060226) do
add_foreign_key "session_activations", "users", name: "fk_e5fda67334", on_delete: :cascade
add_foreign_key "shortcuts", "accounts", on_delete: :cascade
add_foreign_key "status_bookmarks", "accounts", on_delete: :cascade
+ add_foreign_key "status_bookmarks", "status_bookmark_collections", on_delete: :nullify
add_foreign_key "status_bookmarks", "statuses", on_delete: :cascade
add_foreign_key "status_pins", "accounts", name: "fk_d4cb435b62", on_delete: :cascade
add_foreign_key "status_pins", "statuses", on_delete: :cascade
diff --git a/package.json b/package.json
index e62712d8..e1af2715 100644
--- a/package.json
+++ b/package.json
@@ -133,6 +133,7 @@
"tiny-queue": "^0.2.1",
"uglifyjs-webpack-plugin": "^2.1.2",
"uuid": "^3.1.0",
+ "video.js": "^7.10.2",
"webpack": "^4.29.6",
"webpack-assets-manifest": "^3.1.1",
"webpack-bundle-analyzer": "^3.1.0",
diff --git a/yarn.lock b/yarn.lock
index d8098d7a..77d00a6d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -849,6 +849,13 @@
dependencies:
regenerator-runtime "^0.13.4"
+"@babel/runtime@^7.5.5", "@babel/runtime@^7.9.2":
+ version "7.12.5"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e"
+ integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==
+ dependencies:
+ regenerator-runtime "^0.13.4"
+
"@babel/template@^7.8.3", "@babel/template@^7.8.6":
version "7.8.6"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b"
@@ -966,6 +973,38 @@
dependencies:
csstype "^2.2.0"
+"@videojs/http-streaming@2.2.4":
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/@videojs/http-streaming/-/http-streaming-2.2.4.tgz#c71bb63dbc4749e35193c4c334430bd8ce728ec0"
+ integrity sha512-gzT46RpAEegOhMId/zZ6uXCVGDMPOv8qmoTykBuvd6/4lVM3lZ1ZJCq0kytAkisDuDKipy93gP46oZEtonlc/Q==
+ dependencies:
+ "@babel/runtime" "^7.5.5"
+ "@videojs/vhs-utils" "^2.2.1"
+ aes-decrypter "3.1.0"
+ global "^4.3.2"
+ m3u8-parser "4.5.0"
+ mpd-parser "0.14.0"
+ mux.js "5.6.7"
+ video.js "^6 || ^7"
+
+"@videojs/vhs-utils@^2.2.1":
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/@videojs/vhs-utils/-/vhs-utils-2.3.0.tgz#490a3a00dfc1b51d85d5dcf8f8361e2d4c4d1440"
+ integrity sha512-ThSmm91S7tuIJ757ON50K4y7S/bvKN4+B0tu303gCOxaG57PoP1UvPfMQZ90XGhxwNgngexVojOqbBHhTvXVHQ==
+ dependencies:
+ "@babel/runtime" "^7.5.5"
+ global "^4.3.2"
+ url-toolkit "^2.1.6"
+
+"@videojs/xhr@2.5.1":
+ version "2.5.1"
+ resolved "https://registry.yarnpkg.com/@videojs/xhr/-/xhr-2.5.1.tgz#26bc5a79dbb3b03bfb13742c6ce559f89e90719e"
+ integrity sha512-wV9nGESHseSK+S9ePEru2+OJZ1jq/ZbbzniGQ4weAmTIepuBMSYPx5zrxxQA0E786T5ykpO8ts+LayV+3/oI2w==
+ dependencies:
+ "@babel/runtime" "^7.5.5"
+ global "~4.4.0"
+ is-function "^1.0.1"
+
"@webassemblyjs/ast@1.9.0":
version "1.9.0"
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964"
@@ -1149,6 +1188,16 @@ acorn@^7.1.1:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.1.tgz#e35668de0b402f359de515c5482a1ab9f89a69bf"
integrity sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==
+aes-decrypter@3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/aes-decrypter/-/aes-decrypter-3.1.0.tgz#fc0b1d703f97a64aa3f7b13528f4661971db68c4"
+ integrity sha512-wL1NFwP2yNrJG4InpXYFhhYe9TfonnDyhyxMq2+K9/qt+SrZzUieOpviN6pkDly7GawTqw5feehk0rn5iYo00g==
+ dependencies:
+ "@babel/runtime" "^7.5.5"
+ "@videojs/vhs-utils" "^2.2.1"
+ global "^4.3.2"
+ pkcs7 "^1.0.4"
+
ajv-errors@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d"
@@ -2702,6 +2751,11 @@ dom-serializer@0, dom-serializer@^0.2.1:
domelementtype "^2.0.1"
entities "^2.0.0"
+dom-walk@^0.1.0:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.2.tgz#0c548bef048f4d1f2a97249002236060daa3fd84"
+ integrity sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==
+
domain-browser@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
@@ -3620,6 +3674,22 @@ global-prefix@^3.0.0:
kind-of "^6.0.2"
which "^1.3.1"
+global@4.3.2:
+ version "4.3.2"
+ resolved "https://registry.yarnpkg.com/global/-/global-4.3.2.tgz#e76989268a6c74c38908b1305b10fc0e394e9d0f"
+ integrity sha1-52mJJopsdMOJCLEwWxD8DjlOnQ8=
+ dependencies:
+ min-document "^2.19.0"
+ process "~0.5.1"
+
+global@^4.3.1, global@^4.3.2, global@~4.4.0:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/global/-/global-4.4.0.tgz#3e7b105179006a323ed71aafca3e9c57a5cc6406"
+ integrity sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==
+ dependencies:
+ min-document "^2.19.0"
+ process "^0.11.10"
+
globals@^11.1.0, globals@^11.7.0:
version "11.12.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
@@ -4006,6 +4076,11 @@ indexes-of@^1.0.1:
resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc=
+individual@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/individual/-/individual-2.0.0.tgz#833b097dad23294e76117a98fb38e0d9ad61bb97"
+ integrity sha1-gzsJfa0jKU52EXqY+zjg2a1hu5c=
+
infer-owner@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467"
@@ -4276,6 +4351,11 @@ is-fullwidth-code-point@^2.0.0:
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=
+is-function@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-function/-/is-function-1.0.2.tgz#4f097f30abf6efadac9833b17ca5dc03f8144e08"
+ integrity sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==
+
is-glob@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a"
@@ -4512,7 +4592,7 @@ jsx-ast-utils@^2.0.1, jsx-ast-utils@^2.2.1:
array-includes "^3.0.3"
object.assign "^4.1.0"
-keycode@^2.1.7:
+keycode@^2.1.7, keycode@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.2.0.tgz#3d0af56dc7b8b8e5cba8d0a97f107204eec22b04"
integrity sha1-PQr1bce4uOXLqNCpfxByBO7CKwQ=
@@ -4751,6 +4831,15 @@ lru-cache@^5.1.1:
dependencies:
yallist "^3.0.2"
+m3u8-parser@4.5.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/m3u8-parser/-/m3u8-parser-4.5.0.tgz#9c30b32c9b69cc3f81b5e6789717fa84b9fdb9aa"
+ integrity sha512-RGm/1WVCX3o1bSWbJGmJUu4zTbtJy8lImtgHM4CESFvJRXYztr1j6SW/q9/ghYOrUjgH7radsIar+z1Leln0sA==
+ dependencies:
+ "@babel/runtime" "^7.5.5"
+ "@videojs/vhs-utils" "^2.2.1"
+ global "^4.3.2"
+
make-dir@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5"
@@ -4901,6 +4990,13 @@ mimic-fn@^2.0.0:
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
+min-document@^2.19.0:
+ version "2.19.0"
+ resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685"
+ integrity sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=
+ dependencies:
+ dom-walk "^0.1.0"
+
mini-css-extract-plugin@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.5.0.tgz#ac0059b02b9692515a637115b0cc9fed3a35c7b0"
@@ -4997,6 +5093,16 @@ move-concurrently@^1.0.1:
rimraf "^2.5.4"
run-queue "^1.0.3"
+mpd-parser@0.14.0:
+ version "0.14.0"
+ resolved "https://registry.yarnpkg.com/mpd-parser/-/mpd-parser-0.14.0.tgz#f666a80c1e284e46c6f76f010fc4f5292a021148"
+ integrity sha512-HqXQS3WLofcnYFcxv5oWdlciddUaEnN3NasXLVQ793mdnZRrinjz2Yk1DsUYPDYOUWf6ZBBqbFhaJT5LiT2ouA==
+ dependencies:
+ "@babel/runtime" "^7.5.5"
+ "@videojs/vhs-utils" "^2.2.1"
+ global "^4.3.2"
+ xmldom "^0.1.27"
+
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@@ -5030,6 +5136,11 @@ mute-stream@0.0.7:
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=
+mux.js@5.6.7:
+ version "5.6.7"
+ resolved "https://registry.yarnpkg.com/mux.js/-/mux.js-5.6.7.tgz#d39fc85cded5a1257de9f6eeb5cf1578c4a63eb8"
+ integrity sha512-YSr6B8MUgE4S18MptbY2XM+JKGbw9JDkgs7YkuE/T2fpDKjOhZfb/nD6vmsVxvLYOExWNaQn1UGBp6PGsnTtew==
+
nan@^2.12.1:
version "2.14.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c"
@@ -5685,6 +5796,13 @@ pinkie@^2.0.0:
resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA=
+pkcs7@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/pkcs7/-/pkcs7-1.0.4.tgz#6090b9e71160dabf69209d719cbafa538b00a1cb"
+ integrity sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==
+ dependencies:
+ "@babel/runtime" "^7.5.5"
+
pkg-dir@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b"
@@ -6125,6 +6243,11 @@ process@^0.11.10:
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI=
+process@~0.5.1:
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/process/-/process-0.5.2.tgz#1638d8a8e34c2f440a91db95ab9aeb677fc185cf"
+ integrity sha1-FjjYqONML0QKkduVq5rrZ3/Bhc8=
+
progress@^2.0.0:
version "2.0.3"
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
@@ -6898,6 +7021,13 @@ run-queue@^1.0.0, run-queue@^1.0.3:
dependencies:
aproba "^1.1.1"
+rust-result@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/rust-result/-/rust-result-1.0.0.tgz#34c75b2e6dc39fe5875e5bdec85b5e0f91536f72"
+ integrity sha1-NMdbLm3Dn+WHXlveyFteD5FTb3I=
+ dependencies:
+ individual "^2.0.0"
+
rxjs@^6.4.0:
version "6.5.5"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.5.tgz#c5c884e3094c8cfee31bf27eb87e54ccfc87f9ec"
@@ -6915,6 +7045,13 @@ safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1,
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519"
integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==
+safe-json-parse@4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/safe-json-parse/-/safe-json-parse-4.0.0.tgz#7c0f578cfccd12d33a71c0e05413e2eca171eaac"
+ integrity sha1-fA9XjPzNEtM6ccDgVBPi7KFx6qw=
+ dependencies:
+ rust-result "^1.0.0"
+
safe-regex@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
@@ -7920,6 +8057,11 @@ url-parse@^1.4.3:
querystringify "^2.1.1"
requires-port "^1.0.0"
+url-toolkit@^2.1.6:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/url-toolkit/-/url-toolkit-2.2.1.tgz#89009ed3d62a3574de079532a7266c14d2cc1c4f"
+ integrity sha512-8+DzgrtDZYZGhHaAop5WGVghMdCfOLGbhcArsJD0qDll71FXa7EeKxi2hilPIscn2nwMz4PRjML32Sz4JTN0Xw==
+
url@^0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
@@ -8000,6 +8142,32 @@ vendors@^1.0.0:
resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e"
integrity sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==
+"video.js@^6 || ^7", video.js@^7.10.2:
+ version "7.10.2"
+ resolved "https://registry.yarnpkg.com/video.js/-/video.js-7.10.2.tgz#5156aabad7820e726d72ea6c32324059c68885a4"
+ integrity sha512-kJTTrqcQn2MhPzWR8zQs6W3HPJWpowO/ZGZcKt2dcJeJdJT0dEDLYtiFdjV37SylCmu66V0flRnV8cipbthveQ==
+ dependencies:
+ "@babel/runtime" "^7.9.2"
+ "@videojs/http-streaming" "2.2.4"
+ "@videojs/xhr" "2.5.1"
+ global "4.3.2"
+ keycode "^2.2.0"
+ safe-json-parse "4.0.0"
+ videojs-font "3.2.0"
+ videojs-vtt.js "^0.15.2"
+
+videojs-font@3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/videojs-font/-/videojs-font-3.2.0.tgz#212c9d3f4e4ec3fa7345167d64316add35e92232"
+ integrity sha512-g8vHMKK2/JGorSfqAZQUmYYNnXmfec4MLhwtEFS+mMs2IDY398GLysy6BH6K+aS1KMNu/xWZ8Sue/X/mdQPliA==
+
+videojs-vtt.js@^0.15.2:
+ version "0.15.2"
+ resolved "https://registry.yarnpkg.com/videojs-vtt.js/-/videojs-vtt.js-0.15.2.tgz#a828c4ea0aac6303fa471fd69bc7586a5ba1a273"
+ integrity sha512-kEo4hNMvu+6KhPvVYPKwESruwhHC3oFis133LwhXHO9U7nRnx0RiJYMiqbgwjgazDEXHR6t8oGJiHM6wq5XlAw==
+ dependencies:
+ global "^4.3.1"
+
vm-browserify@^1.0.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
@@ -8281,6 +8449,11 @@ ws@^6.0.0, ws@^6.2.1:
dependencies:
async-limiter "~1.0.0"
+xmldom@^0.1.27:
+ version "0.1.31"
+ resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.31.tgz#b76c9a1bd9f0a9737e5a72dc37231cf38375e2ff"
+ integrity sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ==
+
xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"