Progress on little important things

removing .mov for now until we can figure out solution with videojs, added model to track username changes, got chat creation flow down, progress on bookmark collections, albums, filtering blocks/mutes from group, explore, collection timelines
This commit is contained in:
mgabdev 2020-12-22 01:36:38 -05:00
parent 2bbb5be505
commit 6fbea0a59e
37 changed files with 406 additions and 135 deletions

@ -20,7 +20,7 @@ class Api::V1::ChatConversationController < Api::BaseController
def create def create
chat_conversation_account = find_or_create_conversation chat_conversation_account = find_or_create_conversation
render json: chat_conversation_account, each_serializer: REST::ChatConversationAccountSerializer render json: chat_conversation_account, serializer: REST::ChatConversationAccountSerializer
end end
def mark_chat_conversation_read def mark_chat_conversation_read

@ -45,7 +45,11 @@ class Api::V1::Timelines::ExploreController < EmptyController
end end
def explore_statuses def explore_statuses
SortingQueryBuilder.new.call(@sort_type, params[:max_id]) if current_account
SortingQueryBuilder.new.call(@sort_type, params[:max_id]).reject { |status| FeedManager.instance.filter?(:home, status, current_account.id) }
else
SortingQueryBuilder.new.call(@sort_type, params[:max_id])
end
end end
def insert_pagination_headers def insert_pagination_headers

@ -70,7 +70,11 @@ class Api::V1::Timelines::GroupCollectionController < EmptyController
return [] return []
end end
SortingQueryBuilder.new.call(@sort_type, params[:max_id], @groupIds) if current_account
SortingQueryBuilder.new.call(@sort_type, params[:max_id], @groupIds).reject { |status| FeedManager.instance.filter?(:home, status, current_account.id) }
else
SortingQueryBuilder.new.call(@sort_type, params[:max_id], @groupIds)
end
end end
def insert_pagination_headers def insert_pagination_headers

@ -51,7 +51,12 @@ class Api::V1::Timelines::GroupController < Api::BaseController
end end
def group_statuses def group_statuses
SortingQueryBuilder.new.call(@sort_type, params[:max_id], @group) if current_account
SortingQueryBuilder.new.call(@sort_type, params[:max_id], @group).reject { |status| FeedManager.instance.filter?(:home, status, current_account.id) }
else
SortingQueryBuilder.new.call(@sort_type, params[:max_id], @group)
end
end end
def insert_pagination_headers def insert_pagination_headers

@ -36,6 +36,13 @@ class EmptyController < ActionController::Base
protected protected
def set_pagination_headers(next_path = nil, prev_path = nil)
links = []
links << [next_path, [%w(rel next)]] if next_path
links << [prev_path, [%w(rel prev)]] if prev_path
response.headers['Link'] = LinkHeader.new(links) unless links.empty?
end
def current_user def current_user
nil nil
end end

@ -24,11 +24,12 @@ class Settings::ProfilesController < Settings::BaseController
flash[:alert] = 'Unable to change username for your account. You are not GabPRO' flash[:alert] = 'Unable to change username for your account. You are not GabPRO'
redirect_to settings_profile_path redirect_to settings_profile_path
else else
# : todo : if @account.username != params[:account][:username]
# only allowed to change username once per day AccountUsernameChange.create!(
if params[:account][:username] && @account.username != params[:account][:username] account: @account,
Redis.current.set("username_change:#{account.id}", true) from_username: @account.username,
Redis.current.expire("username_change:#{account.id}", 24.huors.seconds) to_username: params[:account][:username]
)
end end
if UpdateAccountService.new.call(@account, account_params) if UpdateAccountService.new.call(@account, account_params)

@ -11,6 +11,8 @@ export const ALBUMS_EXPAND_REQUEST = 'ALBUMS_EXPAND_REQUEST'
export const ALBUMS_EXPAND_SUCCESS = 'ALBUMS_EXPAND_SUCCESS' export const ALBUMS_EXPAND_SUCCESS = 'ALBUMS_EXPAND_SUCCESS'
export const ALBUMS_EXPAND_FAIL = 'ALBUMS_EXPAND_FAIL' export const ALBUMS_EXPAND_FAIL = 'ALBUMS_EXPAND_FAIL'
//
export const ALBUM_CREATE_REQUEST = 'ALBUM_CREATE_REQUEST' export const ALBUM_CREATE_REQUEST = 'ALBUM_CREATE_REQUEST'
export const ALBUM_CREATE_SUCCESS = 'ALBUM_CREATE_SUCCESS' export const ALBUM_CREATE_SUCCESS = 'ALBUM_CREATE_SUCCESS'
export const ALBUM_CREATE_FAIL = 'ALBUM_CREATE_FAIL' export const ALBUM_CREATE_FAIL = 'ALBUM_CREATE_FAIL'
@ -23,7 +25,252 @@ export const ALBUM_EDIT_REQUEST = 'ALBUM_EDIT_REQUEST'
export const ALBUM_EDIT_SUCCESS = 'ALBUM_EDIT_SUCCESS' export const ALBUM_EDIT_SUCCESS = 'ALBUM_EDIT_SUCCESS'
export const ALBUM_EDIT_FAIL = 'ALBUM_EDIT_FAIL' export const ALBUM_EDIT_FAIL = 'ALBUM_EDIT_FAIL'
//
export const ALBUM_UPDATE_MEDIA_REQUEST = 'ALBUM_UPDATE_MEDIA_REQUEST' export const ALBUM_UPDATE_MEDIA_REQUEST = 'ALBUM_UPDATE_MEDIA_REQUEST'
export const ALBUM_UPDATE_MEDIA_SUCCESS = 'ALBUM_UPDATE_MEDIA_SUCCESS' export const ALBUM_UPDATE_MEDIA_SUCCESS = 'ALBUM_UPDATE_MEDIA_SUCCESS'
export const ALBUM_UPDATE_MEDIA_FAIL = 'ALBUM_UPDATE_MEDIA_FAIL' export const ALBUM_UPDATE_MEDIA_FAIL = 'ALBUM_UPDATE_MEDIA_FAIL'
export const SET_ALBUM_COVER_REQUEST = 'SET_ALBUM_COVER_REQUEST'
export const SET_ALBUM_COVER_SUCCESS = 'SET_ALBUM_COVER_SUCCESS'
export const SET_ALBUM_COVER_FAIL = 'SET_ALBUM_COVER_FAIL'
/**
*
*/
export const fetchAlbums = (accountId) => (dispatch, getState) => {
if (!accountId) return
if (getState().getIn(['album_lists', accountId, 'isLoading'])) {
return
}
dispatch(fetchAlbumsRequest(accountId))
api(getState).get(`/api/v1/albums/find_by_account/${accountId}`).then((response) => {
const next = getLinks(response).refs.find(link => link.rel === 'next')
dispatch(fetchAlbumsSuccess(response.data, accountId, next ? next.uri : null))
}).catch((error) => {
dispatch(fetchAlbumsFail(accountId, error))
})
}
const fetchAlbumsRequest = (accountId) => ({
type: ALBUMS_FETCH_REQUEST,
accountId,
})
const fetchAlbumsSuccess = (albums, accountId, next) => ({
type: ALBUMS_FETCH_SUCCESS,
albums,
accountId,
next,
})
const fetchAlbumsFail = (accountId, error) => ({
type: ALBUMS_FETCH_FAIL,
showToast: true,
accountId,
error,
})
/**
*
*/
export const expandAlbums = (accountId) => (dispatch, getState) => {
if (!me) return
const url = getState().getIn(['album_lists', accountId, 'next'], null)
if (url === null || getState().getIn(['album_lists', accountId, 'isLoading'])) {
return
}
dispatch(expandAlbumsRequest(accountId))
api(getState).get(url).then((response) => {
const next = getLinks(response).refs.find(link => link.rel === 'next')
dispatch(expandAlbumsSuccess(response.data, accountId, next ? next.uri : null))
}).catch((error) => {
dispatch(expandAlbumsFail(accountId, error))
})
}
const expandAlbumsRequest = (accountId) => ({
type: ALBUMS_EXPAND_REQUEST,
accountId,
})
const expandAlbumsSuccess = (statuses, accountId, next) => ({
type: ALBUMS_EXPAND_SUCCESS,
accountId,
statuses,
next,
})
const expandAlbumsFail = (accountId, error) => ({
type: ALBUMS_EXPAND_FAIL,
showToast: true,
accountId,
error,
})
/**
*
*/
export const createAlbum = (title, description, visibility) => (dispatch, getState) => {
if (!me || !title) return
dispatch(createAlbumRequest())
api(getState).post('/api/v1/albums', {
title,
description,
visibility,
}).then((response) => {
dispatch(createAlbumSuccess(response.data))
}).catch((error) => {
dispatch(createAlbumFail(error))
})
}
const createAlbumRequest = () => ({
type: ALBUM_CREATE_REQUEST,
})
const createAlbumSuccess = (bookmarkCollection) => ({
type: ALBUM_CREATE_SUCCESS,
bookmarkCollection,
})
const createAlbumFail = (error) => ({
type: ALBUM_CREATE_FAIL,
showToast: true,
error,
})
/**
*
*/
export const editAlbum = (albumId, title, description, visibility) => (dispatch, getState) => {
if (!me || !albumId || !title) return
dispatch(editAlbumRequest(albumId))
api(getState).put(`/api/v1/albums/${albumId}`, {
title,
description,
visibility,
}).then((response) => {
dispatch(editAlbumSuccess(response.data))
}).catch((error) => {
dispatch(editAlbumFail(error))
})
}
const editAlbumRequest = (albumId) => ({
type: ALBUM_EDIT_REQUEST,
albumId,
})
const editAlbumSuccess = (album) => ({
type: ALBUM_EDIT_SUCCESS,
album,
})
const editAlbumFail = (error) => ({
type: ALBUM_EDIT_FAIL,
showToast: true,
error,
})
/**
*
*/
export const removeAlbum = (albumId) => (dispatch, getState) => {
if (!me || !albumId) return
dispatch(removeAlbumRequest(albumId))
api(getState).delete(`/api/v1/albums/${albumId}`).then((response) => {
dispatch(removeAlbumSuccess(response.data))
}).catch((error) => {
dispatch(removeAlbumFail(error))
})
}
const removeAlbumRequest = (albumId) => ({
type: ALBUM_REMOVE_REQUEST,
albumId,
})
const removeAlbumSuccess = () => ({
type: ALBUM_REMOVE_SUCCESS,
})
const removeAlbumFail = (error) => ({
type: ALBUM_REMOVE_FAIL,
showToast: true,
error,
})
/**
*
*/
export const updateMediaAttachmentAlbum = (albumId, mediaAttachmentId) => (dispatch, getState) => {
if (!me || !albumId || !mediaAttachmentId) return
dispatch(updateMediaAttachmentAlbumRequest())
api(getState).post(`/api/v1/albums/${albumId}/update_status`, { statusId }).then((response) => {
dispatch(updateMediaAttachmentAlbumSuccess(response.data))
}).catch((error) => {
dispatch(updateMediaAlbumFail(error))
})
}
const updateMediaAttachmentAlbumRequest = () => ({
type: ALBUM_UPDATE_MEDIA_REQUEST,
})
const updateMediaAttachmentAlbumSuccess = (album) => ({
type: ALBUM_UPDATE_MEDIA_SUCCESS,
album,
})
const updateMediaAlbumFail = (error) => ({
type: ALBUM_UPDATE_MEDIA_FAIL,
showToast: true,
error,
})
/**
*
*/
export const setAlbumCover = (albumId, mediaAttachmentId) => (dispatch, getState) => {
if (!me || !albumId || !mediaAttachmentId) return
dispatch(setAlbumCoverRequest())
api(getState).post(`/api/v1/albums/${albumId}/set_cover`, { mediaAttachmentId }).then((response) => {
dispatch(setAlbumCoverSuccess(response.data))
}).catch((error) => {
dispatch(setAlbumCoverFail(error))
})
}
const setAlbumCoverRequest = () => ({
type: SET_ALBUM_COVER_REQUEST,
})
const setAlbumCoverSuccess = (album) => ({
type: SET_ALBUM_COVER_SUCCESS,
album,
})
const setAlbumCoverFail = (error) => ({
type: SET_ALBUM_COVER_FAIL,
showToast: true,
error,
})

@ -194,16 +194,16 @@ export const removeBookmarkCollection = (bookmarkCollectionId) => (dispatch, get
} }
const removeBookmarkCollectionRequest = (bookmarkCollectionId) => ({ const removeBookmarkCollectionRequest = (bookmarkCollectionId) => ({
type: BOOKMARK_COLLECTIONS_CREATE_REQUEST, type: BOOKMARK_COLLECTIONS_REMOVE_REQUEST,
bookmarkCollectionId, bookmarkCollectionId,
}) })
const removeBookmarkCollectionSuccess = () => ({ const removeBookmarkCollectionSuccess = () => ({
type: BOOKMARK_COLLECTIONS_CREATE_SUCCESS, type: BOOKMARK_COLLECTIONS_REMOVE_SUCCESS,
}) })
const removeBookmarkCollectionFail = (error) => ({ const removeBookmarkCollectionFail = (error) => ({
type: BOOKMARK_COLLECTIONS_CREATE_FAIL, type: BOOKMARK_COLLECTIONS_REMOVE_FAIL,
showToast: true, showToast: true,
error, error,
}) })

@ -41,7 +41,7 @@ export const blockChatMessenger = (accountId) => (dispatch, getState) => {
dispatch(blockChatMessengerRequest(accountId)) dispatch(blockChatMessengerRequest(accountId))
api(getState).post(`/api/v1/chat_conversation_accounts/_/block_messenger`, { account_id: accountId }).then((response) => { api(getState).post(`/api/v1/chat_conversation_accounts/_/block_messenger`, { account_id: accountId }).then((response) => {
dispatch(blockChatMessengerSuccess(response.data)) dispatch(blockChatMessengerSuccess(response))
}).catch((error) => { }).catch((error) => {
dispatch(blockChatMessengerFail(accountId, error)) dispatch(blockChatMessengerFail(accountId, error))
}) })
@ -74,7 +74,7 @@ export const unblockChatMessenger = (accountId) => (dispatch, getState) => {
dispatch(unblockChatMessengerRequest(accountId)) dispatch(unblockChatMessengerRequest(accountId))
api(getState).post(`/api/v1/chat_conversation_accounts/_/unblock_messenger`, { account_id: accountId }).then((response) => { api(getState).post(`/api/v1/chat_conversation_accounts/_/unblock_messenger`, { account_id: accountId }).then((response) => {
dispatch(unblockChatMessengerSuccess(response.data)) dispatch(unblockChatMessengerSuccess(response))
}).catch((error) => { }).catch((error) => {
dispatch(unblockChatMessengerFail(accountId, error)) dispatch(unblockChatMessengerFail(accountId, error))
}) })

@ -1,6 +1,8 @@
import api, { getLinks } from '../api' import api, { getLinks } from '../api'
import debounce from 'lodash.debounce' import debounce from 'lodash.debounce'
import { importFetchedAccounts } from './importer' import { importFetchedAccounts } from './importer'
import { closeModal } from './modal'
import { setChatConversationSelected } from './chats'
import { me } from '../initial_state' import { me } from '../initial_state'
// //
@ -309,15 +311,17 @@ export const expandChatConversationMutedFail = (error) => ({
* @description Create a chat conversation with given accountId. May fail because of blocks. * @description Create a chat conversation with given accountId. May fail because of blocks.
* @param {String} accountId * @param {String} accountId
*/ */
export const createChatConversation = (accountId) => (dispatch, getState) => { export const createChatConversation = (accountId, routerHistory) => (dispatch, getState) => {
if (!me || !accountId) return if (!me || !accountId) return
dispatch(createChatConversationRequest()) dispatch(createChatConversationRequest())
api(getState).post('/api/v1/chat_conversation', { account_id: accountId }).then((response) => { api(getState).post('/api/v1/chat_conversation', { account_id: accountId }).then((response) => {
dispatch(createChatConversationSuccess(response.data)) dispatch(createChatConversationSuccess(response.data))
dispatch(closeModal())
dispatch(setChatConversationSelected(response.data.chat_conversation_id))
if (routerHistory) routerHistory.push(`/messages/${response.data.chat_conversation_id}`)
}).catch((error) => { }).catch((error) => {
console.log("error:", error)
dispatch(createChatConversationFail(error)) dispatch(createChatConversationFail(error))
}) })
} }

@ -88,6 +88,7 @@ const createGroup = (options, routerHistory) => (dispatch, getState) => {
} }
}).then(({ data }) => { }).then(({ data }) => {
dispatch(createGroupSuccess(data)) dispatch(createGroupSuccess(data))
console.log("pushing routerHistory:", routerHistory)
routerHistory.push(`/groups/${data.id}`) routerHistory.push(`/groups/${data.id}`)
}).catch((err) => dispatch(createGroupFail(err))) }).catch((err) => dispatch(createGroupFail(err)))
} }
@ -98,13 +99,13 @@ const createGroupRequest = (id) => ({
id, id,
}) })
const createSuccess = (group) => ({ const createGroupSuccess = (group) => ({
type: GROUP_CREATE_SUCCESS, type: GROUP_CREATE_SUCCESS,
showToast: true, showToast: true,
group, group,
}) })
const createFail = (error) => ({ const createGroupFail = (error) => ({
type: GROUP_CREATE_FAIL, type: GROUP_CREATE_FAIL,
showToast: true, showToast: true,
error, error,
@ -138,7 +139,7 @@ const updateGroup = (groupId, options, routerHistory) => (dispatch, getState) =>
api(getState).put(`/api/v1/groups/${groupId}`, formData, { api(getState).put(`/api/v1/groups/${groupId}`, formData, {
headers: { headers: {
'Content-Type': 'multipart/form-data' 'Content-Type': 'multipart/form-data',
} }
}).then(({ data }) => { }).then(({ data }) => {
dispatch(updateGroupSuccess(data)) dispatch(updateGroupSuccess(data))

@ -1,63 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Sparklines, SparklinesCurve } from 'react-sparklines'
import { FormattedMessage } from 'react-intl'
import ImmutablePropTypes from 'react-immutable-proptypes'
import ImmutablePureComponent from 'react-immutable-pure-component'
import { NavLink } from 'react-router-dom'
import Button from './button'
import Block from './block'
import Text from './text'
class HashtagItem extends ImmutablePureComponent {
render() {
const { hashtag, isCompact } = this.props
if (!hashtag) return
const count = hashtag.get('history').map((block) => {
return parseInt(block.get('uses'))
}).reduce((a, c) => a + c)
return (
<Block>
<div className={[_s.d, _s.w100PC].join(' ')}>
<div className={[_s.d, _s.noUnderline, _s.px15, _s.py5].join(' ')}>
<div className={[_s.d, _s.flexRow, _s.aiCenter].join(' ')}>
<div>
<Text color='brand' size='medium' weight='bold' className={[_s.py2, _s.lineHeight15].join(' ')}>
#{hashtag.get('name')}
</Text>
</div>
</div>
{
!isCompact &&
<Text color='secondary' size='small' className={_s.py2}>
<FormattedMessage id='number_of_gabs' defaultMessage='{count} Gabs' values={{
count,
}} />
</Text>
}
</div>
<Sparklines
width={50}
height={28}
data={hashtag.get('history').reverse().map((day) => day.get('uses')).toArray()}
>
<SparklinesCurve style={{ fill: 'none' }} />
</Sparklines>
</div>
</Block>
)
}
}
HashtagItem.propTypes = {
hashtag: ImmutablePropTypes.map.isRequired,
isCompact: PropTypes.bool,
}
export default HashtagItem

@ -49,6 +49,8 @@ class MediaItem extends ImmutablePureComponent {
if (!attachment) return if (!attachment) return
const hash = attachment.get('blurhash') const hash = attachment.get('blurhash')
if (!hash) return
const pixels = decode(hash, 160, 160) const pixels = decode(hash, 160, 160)
if (pixels && this.canvas) { if (pixels && this.canvas) {
@ -103,7 +105,7 @@ class MediaItem extends ImmutablePureComponent {
const statusUrl = `/${account.getIn(['acct'])}/posts/${status.get('id')}` const statusUrl = `/${account.getIn(['acct'])}/posts/${status.get('id')}`
const isSmallRatio = aspectRatio < 1 const isSmallRatio = aspectRatio < 1
const isSquare = aspectRatio === 1 const isSquare = aspectRatio === 1 || isSmall
const containerClasses = CX({ const containerClasses = CX({
d: 1, d: 1,
px5: 1, px5: 1,

@ -33,12 +33,9 @@ class ChatNavigationBar extends React.PureComponent {
const otherAccounts = chatConversation ? chatConversation.get('other_accounts') : null const otherAccounts = chatConversation ? chatConversation.get('other_accounts') : null
const nameHTML = !!otherAccounts ? otherAccounts.get(0).get('display_name_html') : '' const nameHTML = !!otherAccounts ? otherAccounts.get(0).get('display_name_html') : ''
// : todo :
// fix padding on mobile device
return ( return (
<div className={[_s.d, _s.z4, _s.h53PX, _s.w100PC].join(' ')}> <div className={[_s.d, _s.z4, _s.minH53PX, _s.w100PC].join(' ')}>
<div className={[_s.d, _s.h53PX, _s.bgNavigation, _s.aiCenter, _s.z3, _s.top0, _s.right0, _s.left0, _s.posFixed].join(' ')} > <div className={[_s.d, _s.minH53PX, _s.bgNavigation, _s.aiCenter, _s.z3, _s.top0, _s.right0, _s.left0, _s.posFixed].join(' ')} >
<div className={[_s.d, _s.flexRow, _s.saveAreaInsetPT, _s.saveAreaInsetPL, _s.saveAreaInsetPR, _s.w100PC].join(' ')}> <div className={[_s.d, _s.flexRow, _s.saveAreaInsetPT, _s.saveAreaInsetPL, _s.saveAreaInsetPR, _s.w100PC].join(' ')}>

@ -34,9 +34,9 @@ class ComposeNavigationBar extends React.PureComponent {
}) })
return ( return (
<div className={[_s.d, _s.z4, _s.h53PX, _s.w100PC].join(' ')}> <div className={[_s.d, _s.z4, _s.minH53PX, _s.w100PC].join(' ')}>
<div className={[_s.d, _s.h53PX, _s.bgNavigation, _s.aiCenter, _s.z3, _s.top0, _s.right0, _s.left0, _s.posFixed].join(' ')} > <div className={[_s.d, _s.minH53PX, _s.bgNavigation, _s.aiCenter, _s.z3, _s.top0, _s.right0, _s.left0, _s.posFixed].join(' ')} >
<div className={innerClasses}> <div className={innerClasses}>
<BackButton <BackButton

@ -57,7 +57,7 @@ class MediaGalleryPanel extends ImmutablePureComponent {
headerButtonTitle={!!account ? intl.formatMessage(messages.show_all) : undefined} headerButtonTitle={!!account ? intl.formatMessage(messages.show_all) : undefined}
headerButtonTo={!!account ? `/${account.get('acct')}/albums` : undefined} headerButtonTo={!!account ? `/${account.get('acct')}/albums` : undefined}
> >
<div className={[_s.d, _s.flexRow, _s.flexWrap, _s.px10, _s.py10].join(' ')}> <div className={[_s.d, _s.flexRow, _s.flexWrap, _s.aiCenter, _s.jcCenter].join(' ')}>
{ {
!!account && attachments.size > 0 && !!account && attachments.size > 0 &&
attachments.slice(0, 16).map((attachment, i) => ( attachments.slice(0, 16).map((attachment, i) => (

@ -108,6 +108,7 @@ class Poll extends ImmutablePureComponent {
aiCenter: !showResults, aiCenter: !showResults,
}) })
// : todo : fix widths and truncate for large poll options
return ( return (
<li className={listItemClasses} key={option.get('title')}> <li className={listItemClasses} key={option.get('title')}>
{ {

@ -16,6 +16,7 @@ import {
addShortcut, addShortcut,
removeShortcut, removeShortcut,
} from '../actions/shortcuts' } from '../actions/shortcuts'
import { createChatConversation } from '../actions/chat_conversations'
import { openModal } from '../actions/modal' import { openModal } from '../actions/modal'
import { openPopover } from '../actions/popover' import { openPopover } from '../actions/popover'
import { me } from '../initial_state' import { me } from '../initial_state'
@ -32,6 +33,10 @@ import ProfileHeaderXSPlaceholder from './placeholder/profile_header_xs_placehol
class ProfileHeader extends ImmutablePureComponent { class ProfileHeader extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object
}
state = { state = {
stickied: false, stickied: false,
} }
@ -71,6 +76,15 @@ class ProfileHeader extends ImmutablePureComponent {
} }
} }
handleOnCreateChatConversation = () => {
const { account } = this.props
const accountId = !!account ? account.get('id') : null
if (!accountId) return
this.props.onCreateChatConversation(accountId, this.context.router.history)
}
setOpenMoreNodeRef = (n) => { setOpenMoreNodeRef = (n) => {
this.openMoreNode = n this.openMoreNode = n
} }
@ -226,10 +240,8 @@ class ProfileHeader extends ImmutablePureComponent {
iconClassName={_s.inheritFill} iconClassName={_s.inheritFill}
color='brand' color='brand'
backgroundColor='none' backgroundColor='none'
// : TODO :
className={[_s.jcCenter, _s.aiCenter, _s.mr10, _s.px10].join(' ')} className={[_s.jcCenter, _s.aiCenter, _s.mr10, _s.px10].join(' ')}
onClick={this.handleOpenMore} onClick={this.handleOnCreateChatConversation}
buttonRef={this.setOpenMoreNodeRef}
/> />
</div> </div>
<div className={[_s.d, _s.flexRow, _s.h40PX].join(' ')}> <div className={[_s.d, _s.flexRow, _s.h40PX].join(' ')}>
@ -373,10 +385,8 @@ class ProfileHeader extends ImmutablePureComponent {
iconClassName={_s.inheritFill} iconClassName={_s.inheritFill}
color='brand' color='brand'
backgroundColor='none' backgroundColor='none'
// : TODO :
className={[_s.jcCenter, _s.aiCenter, _s.mr10, _s.px10].join(' ')} className={[_s.jcCenter, _s.aiCenter, _s.mr10, _s.px10].join(' ')}
onClick={this.handleOpenMore} onClick={this.handleOnCreateChatConversation}
buttonRef={this.setOpenMoreNodeRef}
/> />
</div> </div>
} }
@ -435,6 +445,9 @@ const mapDispatchToProps = (dispatch) => ({
onRemoveShortcut(accountId) { onRemoveShortcut(accountId) {
dispatch(removeShortcut(null, 'account', accountId)) dispatch(removeShortcut(null, 'account', accountId))
}, },
onCreateChatConversation(accountId, routerHistory) {
dispatch(createChatConversation(accountId, routerHistory))
},
}); });
ProfileHeader.propTypes = { ProfileHeader.propTypes = {

@ -34,6 +34,7 @@ class StatusCheckBox extends ImmutablePureComponent {
src={video.get('url')} src={video.get('url')}
alt={video.get('description')} alt={video.get('description')}
aspectRatio={video.getIn(['meta', 'small', 'aspect'])} aspectRatio={video.getIn(['meta', 'small', 'aspect'])}
fileContentType={video.get('file_content_type')}
width={239} width={239}
height={110} height={110}
inline inline

@ -67,6 +67,7 @@ class StatusMedia extends ImmutablePureComponent {
src={video.get('url')} src={video.get('url')}
alt={video.get('description')} alt={video.get('description')}
aspectRatio={video.getIn(['meta', 'small', 'aspect'])} aspectRatio={video.getIn(['meta', 'small', 'aspect'])}
fileContentType={video.get('file_content_type')}
sensitive={status.get('sensitive')} sensitive={status.get('sensitive')}
height={110} height={110}
width={width} width={width}

@ -31,7 +31,10 @@ class Video extends ImmutablePureComponent {
} }
componentDidMount() { componentDidMount() {
videoJsOptions.sources = [{ src: this.props.src }] videoJsOptions.sources = [{
src: this.props.src,
type: this.props.fileContentType,
}]
this.videoPlayer = videojs(this.video, videoJsOptions) this.videoPlayer = videojs(this.video, videoJsOptions)
} }
@ -193,6 +196,7 @@ Video.propTypes = {
blurhash: PropTypes.string, blurhash: PropTypes.string,
aspectRatio: PropTypes.number, aspectRatio: PropTypes.number,
meta: ImmutablePropTypes.map, meta: ImmutablePropTypes.map,
fileContentType: PropTypes.string,
} }
export default injectIntl(Video) export default injectIntl(Video)

@ -14,6 +14,10 @@ import Text from '../components/text'
class ChatConversationCreate extends React.PureComponent { class ChatConversationCreate extends React.PureComponent {
static contextTypes = {
router: PropTypes.object
}
state = { state = {
query: '', query: '',
} }
@ -24,7 +28,7 @@ class ChatConversationCreate extends React.PureComponent {
} }
handleOnCreateChatConversation = (accountId) => { handleOnCreateChatConversation = (accountId) => {
this.props.onCreateChatConversation(accountId) this.props.onCreateChatConversation(accountId, this.context.router.history)
this.props.onClearChatConversationAccountSuggestions() this.props.onClearChatConversationAccountSuggestions()
if (this.props.isModal && !!this.props.onCloseModal) { if (this.props.isModal && !!this.props.onCloseModal) {
@ -81,8 +85,8 @@ const mapDispatchToProps = (dispatch) => ({
onChange(value) { onChange(value) {
dispatch(fetchChatConversationAccountSuggestions(value)) dispatch(fetchChatConversationAccountSuggestions(value))
}, },
onCreateChatConversation(accountId) { onCreateChatConversation(accountId, routerHistory) {
dispatch(createChatConversation(accountId)) dispatch(createChatConversation(accountId, routerHistory))
}, },
onClearChatConversationAccountSuggestions() { onClearChatConversationAccountSuggestions() {
dispatch(clearChatConversationAccountSuggestions()) dispatch(clearChatConversationAccountSuggestions())

@ -215,7 +215,7 @@ class Deck extends React.PureComponent {
<DeckColumn title={title} icon='pro' noButtons> <DeckColumn title={title} icon='pro' noButtons>
<div className={[_s.d, _s.px15, _s.py15].join(' ')}> <div className={[_s.d, _s.px15, _s.py15].join(' ')}>
<Text> <Text>
GabDeck is a unique way to customize your Gab experience. Upgrade to GabPRO to unlock the GabDeck. GabDeck is a unique way to customize your Gab experience. Upgrade to GabPRO to unlock the GabDeck.
</Text> </Text>
<div className={[_s.mt15, _s.d, _s.flexRow].join(' ')}> <div className={[_s.mt15, _s.d, _s.flexRow].join(' ')}>
<Button href={URL_GAB_PRO}> <Button href={URL_GAB_PRO}>

@ -6,7 +6,6 @@ import isEqual from 'lodash.isequal'
import { expandHashtagTimeline, clearTimeline } from '../actions/timelines' import { expandHashtagTimeline, clearTimeline } from '../actions/timelines'
import { fetchHashtag } from '../actions/hashtags' import { fetchHashtag } from '../actions/hashtags'
import StatusList from '../components/status_list' import StatusList from '../components/status_list'
import HashtagItem from '../components/hashtag_item'
class HashtagTimeline extends React.PureComponent { class HashtagTimeline extends React.PureComponent {
@ -70,7 +69,7 @@ class HashtagTimeline extends React.PureComponent {
const { id, tags } = this.props.params const { id, tags } = this.props.params
dispatch(expandHashtagTimeline(id, { tags })) dispatch(expandHashtagTimeline(id, { tags }))
dispatch(fetchHashtag(tagName)) // dispatch(fetchHashtag(tagName))
} }
componentWillReceiveProps (nextProps) { componentWillReceiveProps (nextProps) {
@ -94,15 +93,12 @@ class HashtagTimeline extends React.PureComponent {
console.log("tagName:", tag) console.log("tagName:", tag)
return ( return (
<React.Fragment> <StatusList
{ tag && <HashtagItem hashtag={tag} /> } scrollKey='hashtag_timeline'
<StatusList timelineId={`hashtag:${tagName}`}
scrollKey='hashtag_timeline' onLoadMore={this.handleLoadMore}
timelineId={`hashtag:${tagName}`} emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />}
onLoadMore={this.handleLoadMore} />
emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />}
/>
</React.Fragment>
) )
} }
@ -110,7 +106,7 @@ class HashtagTimeline extends React.PureComponent {
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, props) => ({
tagName: props.params.id, tagName: props.params.id,
tag: state.getIn(['hashtags', `${props.params.id}`]), // tag: state.getIn(['hashtags', `${props.params.id}`]),
hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}`, 'unread']) > 0, hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}`, 'unread']) > 0,
}) })

@ -27,8 +27,8 @@ class ChatMessagesComposeForm extends React.PureComponent {
handleOnSendChatMessage = () => { handleOnSendChatMessage = () => {
this.props.onSendChatMessage(this.state.value, this.props.chatConversationId) this.props.onSendChatMessage(this.state.value, this.props.chatConversationId)
document.querySelector('#gabsocial').focus() // document.querySelector('#gabsocial').focus()
this.onBlur() // this.onBlur()
this.setState({ value: '' }) this.setState({ value: '' })
} }

@ -120,7 +120,9 @@ class ChatMessageScrollingList extends ImmutablePureComponent {
} }
scrollToBottom = () => { scrollToBottom = () => {
this.messagesEnd.scrollIntoView({ behavior: 'smooth' }); if (this.messagesEnd) {
this.messagesEnd.scrollIntoView({ behavior: 'smooth' });
}
} }
_selectChild(index, align_top) { _selectChild(index, align_top) {

@ -30,7 +30,7 @@ class MessagesSettings extends ImmutablePureComponent {
<div className={[_s.d, _s.px15, _s.py15, _s.overflowHidden].join(' ')}> <div className={[_s.d, _s.px15, _s.py15, _s.overflowHidden].join(' ')}>
<Form> <Form>
<SettingSwitch <SettingSwitch
label='Restrict messages from people you dont follow' label="Hide chats from users you don't follow"
settings={chatSettings} settings={chatSettings}
settingPath='restrict_non_followers' settingPath='restrict_non_followers'
onChange={this.handleOnChange} onChange={this.handleOnChange}

@ -22,6 +22,7 @@ import {
CHAT_CONVERSATIONS_MUTED_EXPAND_REQUEST, CHAT_CONVERSATIONS_MUTED_EXPAND_REQUEST,
CHAT_CONVERSATIONS_MUTED_EXPAND_SUCCESS, CHAT_CONVERSATIONS_MUTED_EXPAND_SUCCESS,
CHAT_CONVERSATIONS_MUTED_EXPAND_FAIL, CHAT_CONVERSATIONS_MUTED_EXPAND_FAIL,
CHAT_CONVERSATIONS_CREATE_SUCCESS,
} from '../actions/chat_conversations' } from '../actions/chat_conversations'
const initialState = ImmutableMap({ const initialState = ImmutableMap({
@ -103,6 +104,9 @@ export default function chat_conversation_lists(state = initialState, action) {
case CHAT_CONVERSATIONS_MUTED_EXPAND_SUCCESS: case CHAT_CONVERSATIONS_MUTED_EXPAND_SUCCESS:
return appendToList(state, 'muted', action.chatConversations, action.next) return appendToList(state, 'muted', action.chatConversations, action.next)
case CHAT_CONVERSATIONS_CREATE_SUCCESS:
return appendToList(state, 'approved', [action.chatConversation], action.next)
default: default:
return state return state
} }

@ -9,7 +9,8 @@ import {
CHAT_MESSAGES_DELETE_REQUEST, CHAT_MESSAGES_DELETE_REQUEST,
CHAT_MESSAGES_PURGE_REQUEST, CHAT_MESSAGES_PURGE_REQUEST,
} from '../actions/chat_messages' } from '../actions/chat_messages'
import { import {
CHAT_CONVERSATIONS_CREATE_SUCCESS,
CHAT_CONVERSATIONS_APPROVED_FETCH_SUCCESS, CHAT_CONVERSATIONS_APPROVED_FETCH_SUCCESS,
CHAT_CONVERSATIONS_APPROVED_EXPAND_SUCCESS, CHAT_CONVERSATIONS_APPROVED_EXPAND_SUCCESS,
CHAT_CONVERSATIONS_REQUESTED_FETCH_SUCCESS, CHAT_CONVERSATIONS_REQUESTED_FETCH_SUCCESS,
@ -43,6 +44,7 @@ export default function chat_conversations(state = initialState, action) {
switch(action.type) { switch(action.type) {
case CHAT_CONVERSATION_REQUEST_APPROVE_SUCCESS: case CHAT_CONVERSATION_REQUEST_APPROVE_SUCCESS:
case SET_CHAT_CONVERSATION_EXPIRATION_SUCCESS: case SET_CHAT_CONVERSATION_EXPIRATION_SUCCESS:
case CHAT_CONVERSATIONS_CREATE_SUCCESS:
return importChatConversation(state, action.chatConversation) return importChatConversation(state, action.chatConversation)
case CHAT_CONVERSATIONS_APPROVED_FETCH_SUCCESS: case CHAT_CONVERSATIONS_APPROVED_FETCH_SUCCESS:
case CHAT_CONVERSATIONS_APPROVED_EXPAND_SUCCESS: case CHAT_CONVERSATIONS_APPROVED_EXPAND_SUCCESS:

@ -0,0 +1,18 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: account_username_changes
#
# id :bigint(8) not null, primary key
# account_id :bigint(8) not null
# from_username :text default(""), not null
# to_username :text default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
#
class AccountUsernameChange < ApplicationRecord
belongs_to :account
end

@ -31,7 +31,8 @@ class Group < ApplicationRecord
include GroupInteractions include GroupInteractions
include GroupCoverImage include GroupCoverImage
PER_ACCOUNT_LIMIT = 50 PER_ACCOUNT_LIMIT_PRO = 100
PER_ACCOUNT_LIMIT_NORMAL = 10
belongs_to :account, optional: true belongs_to :account, optional: true
@ -53,7 +54,9 @@ class Group < ApplicationRecord
validates :description, presence: true validates :description, presence: true
validates_each :account_id, on: :create do |record, _attr, value| validates_each :account_id, on: :create do |record, _attr, value|
record.errors.add(:base, I18n.t('groups.errors.limit')) if Group.where(account_id: value).count >= PER_ACCOUNT_LIMIT account = Account.find(value)
limit = account.is_pro ? PER_ACCOUNT_LIMIT_PRO : PER_ACCOUNT_LIMIT_NORMAL
record.errors.add(:base, "You have reached the limit for group creation.") if Group.where(account_id: value).count >= limit
end end
before_save :set_slug before_save :set_slug

@ -28,11 +28,11 @@ class MediaAttachment < ApplicationRecord
enum type: [:image, :gifv, :video, :unknown] enum type: [:image, :gifv, :video, :unknown]
IMAGE_FILE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp'].freeze IMAGE_FILE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp'].freeze
VIDEO_FILE_EXTENSIONS = ['.webm', '.mp4', '.m4v', '.mov'].freeze VIDEO_FILE_EXTENSIONS = ['.webm', '.mp4', '.m4v'].freeze
IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze
VIDEO_MIME_TYPES = ['video/webm', 'video/mp4', 'video/quicktime'].freeze VIDEO_MIME_TYPES = ['video/webm', 'video/mp4'].freeze
VIDEO_CONVERTIBLE_MIME_TYPES = ['video/webm', 'video/quicktime'].freeze VIDEO_CONVERTIBLE_MIME_TYPES = ['video/webm'].freeze
BLURHASH_OPTIONS = { BLURHASH_OPTIONS = {
x_comp: 4, x_comp: 4,
@ -170,6 +170,7 @@ class MediaAttachment < ApplicationRecord
elsif IMAGE_MIME_TYPES.include? f.instance.file_content_type elsif IMAGE_MIME_TYPES.include? f.instance.file_content_type
IMAGE_STYLES IMAGE_STYLES
elsif VIDEO_CONVERTIBLE_MIME_TYPES.include?(f.instance.file_content_type) elsif VIDEO_CONVERTIBLE_MIME_TYPES.include?(f.instance.file_content_type)
puts "tilly convert"
{ {
small: VIDEO_STYLES[:small], small: VIDEO_STYLES[:small],
original: VIDEO_FORMAT, original: VIDEO_FORMAT,

@ -5,7 +5,7 @@ class REST::MediaAttachmentSerializer < ActiveModel::Serializer
attributes :id, :type, :url, :preview_url, attributes :id, :type, :url, :preview_url,
:remote_url, :text_url, :meta, :remote_url, :text_url, :meta,
:description, :blurhash :description, :blurhash, :file_content_type
def id def id
object.id.to_s object.id.to_s

@ -0,0 +1,10 @@
class CreateAccountUsernameChanges < ActiveRecord::Migration[5.2]
def change
create_table :account_username_changes do |t|
t.belongs_to :account, foreign_key: { on_delete: :cascade }, null: false
t.text :from_username, null: false, default: ''
t.text :to_username, null: false, default: ''
t.timestamps
end
end
end

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2020_12_18_012018) do ActiveRecord::Schema.define(version: 2020_12_22_040559) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "pg_stat_statements" enable_extension "pg_stat_statements"
@ -59,6 +59,15 @@ ActiveRecord::Schema.define(version: 2020_12_18_012018) do
t.index ["tag_id"], name: "index_account_tag_stats_on_tag_id", unique: true t.index ["tag_id"], name: "index_account_tag_stats_on_tag_id", unique: true
end end
create_table "account_username_changes", force: :cascade do |t|
t.bigint "account_id", null: false
t.text "from_username", default: "", null: false
t.text "to_username", default: "", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["account_id"], name: "index_account_username_changes_on_account_id"
end
create_table "account_verification_requests", force: :cascade do |t| create_table "account_verification_requests", force: :cascade do |t|
t.bigint "account_id" t.bigint "account_id"
t.string "image_file_name" t.string "image_file_name"
@ -873,6 +882,7 @@ ActiveRecord::Schema.define(version: 2020_12_18_012018) do
add_foreign_key "account_moderation_notes", "accounts", column: "target_account_id" add_foreign_key "account_moderation_notes", "accounts", column: "target_account_id"
add_foreign_key "account_stats", "accounts", on_delete: :cascade add_foreign_key "account_stats", "accounts", on_delete: :cascade
add_foreign_key "account_tag_stats", "tags", on_delete: :cascade add_foreign_key "account_tag_stats", "tags", on_delete: :cascade
add_foreign_key "account_username_changes", "accounts", on_delete: :cascade
add_foreign_key "account_warnings", "accounts", column: "target_account_id", on_delete: :cascade add_foreign_key "account_warnings", "accounts", column: "target_account_id", on_delete: :cascade
add_foreign_key "account_warnings", "accounts", on_delete: :nullify add_foreign_key "account_warnings", "accounts", on_delete: :nullify
add_foreign_key "accounts", "accounts", column: "moved_to_account_id", on_delete: :nullify add_foreign_key "accounts", "accounts", column: "moved_to_account_id", on_delete: :nullify

@ -114,7 +114,6 @@
"react-router-dom": "^4.1.1", "react-router-dom": "^4.1.1",
"react-router-scroll-4": "^1.0.0-beta.2", "react-router-scroll-4": "^1.0.0-beta.2",
"react-sortable-hoc": "^1.11.0", "react-sortable-hoc": "^1.11.0",
"react-sparklines": "^1.7.0",
"react-stickynode": "^3.0.4", "react-stickynode": "^3.0.4",
"react-swipeable-views": "^0.13.9", "react-swipeable-views": "^0.13.9",
"react-textarea-autosize": "^7.1.0", "react-textarea-autosize": "^7.1.0",

@ -6618,13 +6618,6 @@ react-sortable-hoc@^1.11.0:
invariant "^2.2.4" invariant "^2.2.4"
prop-types "^15.5.7" prop-types "^15.5.7"
react-sparklines@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/react-sparklines/-/react-sparklines-1.7.0.tgz#9b1d97e8c8610095eeb2ad658d2e1fcf91f91a60"
integrity sha512-bJFt9K4c5Z0k44G8KtxIhbG+iyxrKjBZhdW6afP+R7EnIq+iKjbWbEFISrf3WKNFsda+C46XAfnX0StS5fbDcg==
dependencies:
prop-types "^15.5.10"
react-stickynode@^3.0.4: react-stickynode@^3.0.4:
version "3.0.4" version "3.0.4"
resolved "https://registry.yarnpkg.com/react-stickynode/-/react-stickynode-3.0.4.tgz#1e9c096cec3613cc8294807eba319ced074c8b21" resolved "https://registry.yarnpkg.com/react-stickynode/-/react-stickynode-3.0.4.tgz#1e9c096cec3613cc8294807eba319ced074c8b21"