mirror of
https://github.com/mastodon/mastodon.git
synced 2024-12-22 22:15:23 +01:00
Add unread indicator to conversations (#9009)
This commit is contained in:
parent
bebe8ec887
commit
a38a452481
13 changed files with 98 additions and 11 deletions
|
@ -3,9 +3,11 @@
|
|||
class Api::V1::ConversationsController < Api::BaseController
|
||||
LIMIT = 20
|
||||
|
||||
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }
|
||||
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: :index
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:conversations' }, except: :index
|
||||
before_action :require_user!
|
||||
after_action :insert_pagination_headers
|
||||
before_action :set_conversation, except: :index
|
||||
after_action :insert_pagination_headers, only: :index
|
||||
|
||||
respond_to :json
|
||||
|
||||
|
@ -14,8 +16,22 @@ class Api::V1::ConversationsController < Api::BaseController
|
|||
render json: @conversations, each_serializer: REST::ConversationSerializer
|
||||
end
|
||||
|
||||
def read
|
||||
@conversation.update!(unread: false)
|
||||
render json: @conversation, serializer: REST::ConversationSerializer
|
||||
end
|
||||
|
||||
def destroy
|
||||
@conversation.destroy!
|
||||
render_empty
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_conversation
|
||||
@conversation = AccountConversation.where(account: current_account).find(params[:id])
|
||||
end
|
||||
|
||||
def paginated_conversations
|
||||
AccountConversation.where(account: current_account)
|
||||
.paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::ReportsController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :read, :'read:reports' }, except: [:create]
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:reports' }, only: [:create]
|
||||
before_action :require_user!
|
||||
|
||||
|
|
|
@ -13,6 +13,8 @@ export const CONVERSATIONS_FETCH_SUCCESS = 'CONVERSATIONS_FETCH_SUCCESS';
|
|||
export const CONVERSATIONS_FETCH_FAIL = 'CONVERSATIONS_FETCH_FAIL';
|
||||
export const CONVERSATIONS_UPDATE = 'CONVERSATIONS_UPDATE';
|
||||
|
||||
export const CONVERSATIONS_READ = 'CONVERSATIONS_READ';
|
||||
|
||||
export const mountConversations = () => ({
|
||||
type: CONVERSATIONS_MOUNT,
|
||||
});
|
||||
|
@ -21,6 +23,15 @@ export const unmountConversations = () => ({
|
|||
type: CONVERSATIONS_UNMOUNT,
|
||||
});
|
||||
|
||||
export const markConversationRead = conversationId => (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: CONVERSATIONS_READ,
|
||||
id: conversationId,
|
||||
});
|
||||
|
||||
api(getState).post(`/api/v1/conversations/${conversationId}/read`);
|
||||
};
|
||||
|
||||
export const expandConversations = ({ maxId } = {}) => (dispatch, getState) => {
|
||||
dispatch(expandConversationsRequest());
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import DisplayName from '../../../components/display_name';
|
|||
import Avatar from '../../../components/avatar';
|
||||
import AttachmentList from '../../../components/attachment_list';
|
||||
import { HotKeys } from 'react-hotkeys';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default class Conversation extends ImmutablePureComponent {
|
||||
|
||||
|
@ -19,8 +20,10 @@ export default class Conversation extends ImmutablePureComponent {
|
|||
conversationId: PropTypes.string.isRequired,
|
||||
accounts: ImmutablePropTypes.list.isRequired,
|
||||
lastStatus: ImmutablePropTypes.map.isRequired,
|
||||
unread:PropTypes.bool.isRequired,
|
||||
onMoveUp: PropTypes.func,
|
||||
onMoveDown: PropTypes.func,
|
||||
markRead: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
|
@ -28,7 +31,12 @@ export default class Conversation extends ImmutablePureComponent {
|
|||
return;
|
||||
}
|
||||
|
||||
const { lastStatus } = this.props;
|
||||
const { lastStatus, unread, markRead } = this.props;
|
||||
|
||||
if (unread) {
|
||||
markRead();
|
||||
}
|
||||
|
||||
this.context.router.history.push(`/statuses/${lastStatus.get('id')}`);
|
||||
}
|
||||
|
||||
|
@ -41,7 +49,7 @@ export default class Conversation extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { accounts, lastStatus, lastAccount } = this.props;
|
||||
const { accounts, lastStatus, lastAccount, unread } = this.props;
|
||||
|
||||
if (lastStatus === null) {
|
||||
return null;
|
||||
|
@ -61,7 +69,7 @@ export default class Conversation extends ImmutablePureComponent {
|
|||
|
||||
return (
|
||||
<HotKeys handlers={handlers}>
|
||||
<div className='conversation focusable' tabIndex='0' onClick={this.handleClick} role='button'>
|
||||
<div className={classNames('conversation', 'focusable', { 'conversation--unread': unread })} tabIndex='0' onClick={this.handleClick} role='button'>
|
||||
<div className='conversation__header'>
|
||||
<div className='conversation__avatars'>
|
||||
<div>{accounts.map(account => <Avatar key={account.get('id')} size={36} account={account} />)}</div>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { connect } from 'react-redux';
|
||||
import Conversation from '../components/conversation';
|
||||
import { markConversationRead } from '../../../actions/conversations';
|
||||
|
||||
const mapStateToProps = (state, { conversationId }) => {
|
||||
const conversation = state.getIn(['conversations', 'items']).find(x => x.get('id') === conversationId);
|
||||
|
@ -7,9 +8,14 @@ const mapStateToProps = (state, { conversationId }) => {
|
|||
|
||||
return {
|
||||
accounts: conversation.get('accounts').map(accountId => state.getIn(['accounts', accountId], null)),
|
||||
unread: conversation.get('unread'),
|
||||
lastStatus,
|
||||
lastAccount: lastStatus === null ? null : state.getIn(['accounts', lastStatus.get('account')], null),
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(Conversation);
|
||||
const mapDispatchToProps = (dispatch, { conversationId }) => ({
|
||||
markRead: () => dispatch(markConversationRead(conversationId)),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Conversation);
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
CONVERSATIONS_FETCH_SUCCESS,
|
||||
CONVERSATIONS_FETCH_FAIL,
|
||||
CONVERSATIONS_UPDATE,
|
||||
CONVERSATIONS_READ,
|
||||
} from '../actions/conversations';
|
||||
import compareId from '../compare_id';
|
||||
|
||||
|
@ -18,6 +19,7 @@ const initialState = ImmutableMap({
|
|||
|
||||
const conversationToMap = item => ImmutableMap({
|
||||
id: item.id,
|
||||
unread: item.unread,
|
||||
accounts: ImmutableList(item.accounts.map(a => a.id)),
|
||||
last_status: item.last_status.id,
|
||||
});
|
||||
|
@ -80,6 +82,14 @@ export default function conversations(state = initialState, action) {
|
|||
return state.update('mounted', count => count + 1);
|
||||
case CONVERSATIONS_UNMOUNT:
|
||||
return state.update('mounted', count => count - 1);
|
||||
case CONVERSATIONS_READ:
|
||||
return state.update('items', list => list.map(item => {
|
||||
if (item.get('id') === action.id) {
|
||||
return item.set('unread', false);
|
||||
}
|
||||
|
||||
return item;
|
||||
}));
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -5503,6 +5503,11 @@ noscript {
|
|||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||
cursor: pointer;
|
||||
|
||||
&--unread {
|
||||
background: lighten($ui-base-color, 8%);
|
||||
border-bottom-color: lighten($ui-base-color, 12%);
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
margin-bottom: 15px;
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
# status_ids :bigint(8) default([]), not null, is an Array
|
||||
# last_status_id :bigint(8)
|
||||
# lock_version :integer default(0), not null
|
||||
# unread :boolean default(FALSE), not null
|
||||
#
|
||||
|
||||
class AccountConversation < ApplicationRecord
|
||||
|
@ -58,6 +59,7 @@ class AccountConversation < ApplicationRecord
|
|||
def add_status(recipient, status)
|
||||
conversation = find_or_initialize_by(account: recipient, conversation_id: status.conversation_id, participant_account_ids: participants_from_status(recipient, status))
|
||||
conversation.status_ids << status.id
|
||||
conversation.unread = status.account_id != recipient.id
|
||||
conversation.save
|
||||
conversation
|
||||
rescue ActiveRecord::StaleObjectError
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class REST::ConversationSerializer < ActiveModel::Serializer
|
||||
attribute :id
|
||||
attributes :id, :unread
|
||||
|
||||
has_many :participant_accounts, key: :accounts, serializer: REST::AccountSerializer
|
||||
has_one :last_status, serializer: REST::StatusSerializer
|
||||
|
||||
|
|
|
@ -58,6 +58,7 @@ Doorkeeper.configure do
|
|||
optional_scopes :write,
|
||||
:'write:accounts',
|
||||
:'write:blocks',
|
||||
:'write:conversations',
|
||||
:'write:favourites',
|
||||
:'write:filters',
|
||||
:'write:follows',
|
||||
|
@ -76,7 +77,6 @@ Doorkeeper.configure do
|
|||
:'read:lists',
|
||||
:'read:mutes',
|
||||
:'read:notifications',
|
||||
:'read:reports',
|
||||
:'read:search',
|
||||
:'read:statuses',
|
||||
:follow,
|
||||
|
|
|
@ -261,7 +261,12 @@ Rails.application.routes.draw do
|
|||
resources :streaming, only: [:index]
|
||||
resources :custom_emojis, only: [:index]
|
||||
resources :suggestions, only: [:index, :destroy]
|
||||
resources :conversations, only: [:index]
|
||||
|
||||
resources :conversations, only: [:index, :destroy] do
|
||||
member do
|
||||
post :read
|
||||
end
|
||||
end
|
||||
|
||||
get '/search', to: 'search#index', as: :search
|
||||
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
require Rails.root.join('lib', 'mastodon', 'migration_helpers')
|
||||
|
||||
class AddUnreadToAccountConversations < ActiveRecord::Migration[5.2]
|
||||
include Mastodon::MigrationHelpers
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
safety_assured do
|
||||
add_column_with_default(
|
||||
:account_conversations,
|
||||
:unread,
|
||||
:boolean,
|
||||
allow_null: false,
|
||||
default: false
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
remove_column :account_conversations, :unread, :boolean
|
||||
end
|
||||
end
|
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 2018_10_10_141500) do
|
||||
ActiveRecord::Schema.define(version: 2018_10_18_205649) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
@ -22,6 +22,7 @@ ActiveRecord::Schema.define(version: 2018_10_10_141500) do
|
|||
t.bigint "status_ids", default: [], null: false, array: true
|
||||
t.bigint "last_status_id"
|
||||
t.integer "lock_version", default: 0, null: false
|
||||
t.boolean "unread", default: false, null: false
|
||||
t.index ["account_id", "conversation_id", "participant_account_ids"], name: "index_unique_conversations", unique: true
|
||||
t.index ["account_id"], name: "index_account_conversations_on_account_id"
|
||||
t.index ["conversation_id"], name: "index_account_conversations_on_conversation_id"
|
||||
|
|
Loading…
Reference in a new issue