PLT-3114 Moved preview collapse out of pre-release features (#3206)

* Added user setting to auto collapse image previews

* Added slash commands for collapsing/expanding image previews

* Added translation strings for collapse setting

* Add default props for preview collapse setting
Этот коммит содержится в:
Joram Wilander
2016-06-06 13:41:54 -04:00
коммит произвёл Corey Hulen
родитель 629b49a119
Коммит 1e245f19c7
14 изменённых файлов: 353 добавлений и 43 удалений

77
api/command_expand_collapse.go Обычный файл
Просмотреть файл

@@ -0,0 +1,77 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package api
import (
"github.com/mattermost/platform/model"
)
type ExpandProvider struct {
}
type CollapseProvider struct {
}
const (
CMD_EXPAND = "expand"
CMD_COLLAPSE = "collapse"
)
func init() {
RegisterCommandProvider(&ExpandProvider{})
RegisterCommandProvider(&CollapseProvider{})
}
func (me *ExpandProvider) GetTrigger() string {
return CMD_EXPAND
}
func (me *CollapseProvider) GetTrigger() string {
return CMD_COLLAPSE
}
func (me *ExpandProvider) GetCommand(c *Context) *model.Command {
return &model.Command{
Trigger: CMD_EXPAND,
AutoComplete: true,
AutoCompleteDesc: c.T("api.command_expand.desc"),
DisplayName: c.T("api.command_expand.name"),
}
}
func (me *CollapseProvider) GetCommand(c *Context) *model.Command {
return &model.Command{
Trigger: CMD_COLLAPSE,
AutoComplete: true,
AutoCompleteDesc: c.T("api.command_collapse.desc"),
DisplayName: c.T("api.command_collapse.name"),
}
}
func (me *ExpandProvider) DoCommand(c *Context, channelId string, message string) *model.CommandResponse {
return setCollapsePreference(c, "false")
}
func (me *CollapseProvider) DoCommand(c *Context, channelId string, message string) *model.CommandResponse {
return setCollapsePreference(c, "true")
}
func setCollapsePreference(c *Context, value string) *model.CommandResponse {
pref := model.Preference{
UserId: c.Session.UserId,
Category: model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS,
Name: model.PREFERENCE_NAME_COLLAPSE_SETTING,
Value: value,
}
if result := <-Srv.Store.Preference().Save(&model.Preferences{pref}); result.Err != nil {
return &model.CommandResponse{Text: c.T("api.command_expand_collapse.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
}
socketMessage := model.NewMessage("", "", c.Session.UserId, model.ACTION_PREFERENCE_CHANGED)
socketMessage.Add("preference", pref.ToJson())
go Publish(socketMessage)
return &model.CommandResponse{}
}

47
api/command_expand_collapse_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,47 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package api
import (
"testing"
"time"
"github.com/mattermost/platform/model"
)
func TestExpandCommand(t *testing.T) {
th := Setup().InitBasic()
Client := th.BasicClient
channel := th.BasicChannel
r1 := Client.Must(Client.Command(channel.Id, "/expand", false)).Data.(*model.CommandResponse)
if r1 == nil {
t.Fatal("Command failed to execute")
}
time.Sleep(100 * time.Millisecond)
p1 := Client.Must(Client.GetPreference(model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS, model.PREFERENCE_NAME_COLLAPSE_SETTING)).Data.(*model.Preference)
if p1.Value != "false" {
t.Fatal("preference not updated correctly")
}
}
func TestCollapseCommand(t *testing.T) {
th := Setup().InitBasic()
Client := th.BasicClient
channel := th.BasicChannel
r1 := Client.Must(Client.Command(channel.Id, "/collapse", false)).Data.(*model.CommandResponse)
if r1 == nil {
t.Fatal("Command failed to execute")
}
time.Sleep(100 * time.Millisecond)
p1 := Client.Must(Client.GetPreference(model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS, model.PREFERENCE_NAME_COLLAPSE_SETTING)).Data.(*model.Preference)
if p1.Value != "true" {
t.Fatal("preference not updated correctly")
}
}

Просмотреть файл

@@ -399,6 +399,26 @@
"id": "api.command_echo.name",
"translation": "echo"
},
{
"id": "api.command_collapse.desc",
"translation": "Turn on auto-collapsing of image previews"
},
{
"id": "api.command_collapse.name",
"translation": "collapse"
},
{
"id": "api.command_expand.desc",
"translation": "Turn off auto-collapsing of image previews"
},
{
"id": "api.command_expand_collapse.fail.app_error",
"translation": "An error occured while expanding previews"
},
{
"id": "api.command_expand.name",
"translation": "expand"
},
{
"id": "api.command_join.desc",
"translation": "Join the open channel"

Просмотреть файл

@@ -14,6 +14,9 @@ const (
PREFERENCE_CATEGORY_TUTORIAL_STEPS = "tutorial_step"
PREFERENCE_CATEGORY_ADVANCED_SETTINGS = "advanced_settings"
PREFERENCE_CATEGORY_DISPLAY_SETTINGS = "display_settings"
PREFERENCE_NAME_COLLAPSE_SETTING = "collapse_previews"
PREFERENCE_CATEGORY_LAST = "last"
PREFERENCE_NAME_LAST_CHANNEL = "channel"
)

Просмотреть файл

@@ -72,6 +72,10 @@ export default class Post extends React.Component {
return true;
}
if (nextProps.previewCollapsed !== this.props.previewCollapsed) {
return true;
}
if (!Utils.areObjectsEqual(nextProps.user, this.props.user)) {
return true;
}
@@ -190,6 +194,7 @@ export default class Post extends React.Component {
parentPost={parentPost}
handleCommentClick={this.handleCommentClick}
compactDisplay={this.props.compactDisplay}
previewCollapsed={this.props.previewCollapsed}
/>
</div>
</div>
@@ -213,5 +218,6 @@ Post.propTypes = {
currentUser: React.PropTypes.object.isRequired,
center: React.PropTypes.bool,
compactDisplay: React.PropTypes.bool,
previewCollapsed: React.PropTypes.string,
commentCount: React.PropTypes.number
};

Просмотреть файл

@@ -25,7 +25,11 @@ export default class PostBody extends React.Component {
return true;
}
if (!Utils.areObjectsEqual(nextProps.compactDisplay, this.props.compactDisplay)) {
if (nextProps.compactDisplay !== this.props.compactDisplay) {
return true;
}
if (nextProps.previewCollapsed !== this.props.previewCollapsed) {
return true;
}
@@ -172,6 +176,7 @@ export default class PostBody extends React.Component {
post={this.props.post}
message={messageWrapper}
compactDisplay={this.props.compactDisplay}
previewCollapsed={this.props.previewCollapsed}
/>
);
}
@@ -193,5 +198,6 @@ PostBody.propTypes = {
parentPost: React.PropTypes.object,
retryPost: React.PropTypes.func.isRequired,
handleCommentClick: React.PropTypes.func.isRequired,
compactDisplay: React.PropTypes.bool
compactDisplay: React.PropTypes.bool,
previewCollapsed: React.PropTypes.string
};

Просмотреть файл

@@ -22,10 +22,14 @@ export default class PostBodyAdditionalContent extends React.Component {
this.toggleEmbedVisibility = this.toggleEmbedVisibility.bind(this);
this.state = {
embedVisible: true
embedVisible: props.previewCollapsed.startsWith('false')
};
}
componentWillReceiveProps(nextProps) {
this.setState({embedVisible: nextProps.previewCollapsed.startsWith('false')});
}
shouldComponentUpdate(nextProps, nextState) {
if (!Utils.areObjectsEqual(nextProps.post, this.props.post)) {
return true;
@@ -118,25 +122,23 @@ export default class PostBodyAdditionalContent extends React.Component {
if (generateEmbed) {
let messageWithToggle = [];
if (Utils.isFeatureEnabled(Constants.PRE_RELEASE_FEATURES.EMBED_TOGGLE)) {
// if message has only one line and starts with a link place toggle in this only line
// else - place it in new line between message and embed
const prependToggle = (/^\s*https?:\/\/.*$/).test(this.props.post.message);
messageWithToggle.push(
<a
className={`post__embed-visibility ${prependToggle ? 'pull-left' : ''}`}
data-expanded={this.state.embedVisible}
aria-label='Toggle Embed Visibility'
onClick={this.toggleEmbedVisibility}
/>
);
if (prependToggle) {
messageWithToggle.push(this.props.message);
} else {
messageWithToggle.unshift(this.props.message);
}
} else {
// if message has only one line and starts with a link place toggle in this only line
// else - place it in new line between message and embed
const prependToggle = (/^\s*https?:\/\/.*$/).test(this.props.post.message);
messageWithToggle.push(
<a
className={`post__embed-visibility ${prependToggle ? 'pull-left' : ''}`}
data-expanded={this.state.embedVisible}
aria-label='Toggle Embed Visibility'
onClick={this.toggleEmbedVisibility}
/>
);
if (prependToggle) {
messageWithToggle.push(this.props.message);
} else {
messageWithToggle.unshift(this.props.message);
}
return (
@@ -156,8 +158,12 @@ export default class PostBodyAdditionalContent extends React.Component {
}
}
PostBodyAdditionalContent.defaultProps = {
previewCollapsed: 'false'
};
PostBodyAdditionalContent.propTypes = {
post: React.PropTypes.object.isRequired,
message: React.PropTypes.element.isRequired,
compactDisplay: React.PropTypes.bool,
message: React.PropTypes.element.isRequired
previewCollapsed: React.PropTypes.string
};

Просмотреть файл

@@ -260,6 +260,7 @@ export default class PostList extends React.Component {
center={this.props.displayPostsInCenter}
commentCount={commentCount}
compactDisplay={this.props.compactDisplay}
previewCollapsed={this.props.previewsCollapsed}
/>
);
@@ -520,5 +521,6 @@ PostList.propTypes = {
postsToHighlight: React.PropTypes.object,
displayNameType: React.PropTypes.string,
displayPostsInCenter: React.PropTypes.bool,
compactDisplay: React.PropTypes.bool
compactDisplay: React.PropTypes.bool,
previewsCollapsed: React.PropTypes.string
};

Просмотреть файл

@@ -51,7 +51,8 @@ export default class PostViewController extends React.Component {
scrollType: ScrollTypes.NEW_MESSAGE,
displayNameType: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'false'),
displayPostsInCenter: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_CENTERED,
compactDisplay: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT
compactDisplay: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT,
previewsCollapsed: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.COLLAPSE_DISPLAY, 'false')
};
}
@@ -67,11 +68,19 @@ export default class PostViewController extends React.Component {
}
}
onPreferenceChange() {
onPreferenceChange(category) {
// Bit of a hack to force render when this setting is updated
// regardless of change
let previewSuffix = '';
if (category === Preferences.CATEGORY_DISPLAY_SETTINGS) {
previewSuffix = '_' + Utils.generateId();
}
this.setState({
displayNameType: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'false'),
displayPostsInCenter: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_CENTERED,
compactDisplay: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT
compactDisplay: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT,
previewsCollapsed: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.COLLAPSE_DISPLAY, 'false') + previewSuffix
});
}
@@ -132,6 +141,7 @@ export default class PostViewController extends React.Component {
displayNameType: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'false'),
displayPostsInCenter: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_CENTERED,
compactDisplay: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT,
previewsCollapsed: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.COLLAPSE_DISPLAY, 'false'),
scrollType: ScrollTypes.NEW_MESSAGE
});
}
@@ -183,6 +193,10 @@ export default class PostViewController extends React.Component {
return true;
}
if (nextState.previewsCollapsed !== this.state.previewsCollapsed) {
return true;
}
if (nextState.lastViewed !== this.state.lastViewed) {
return true;
}
@@ -241,6 +255,7 @@ export default class PostViewController extends React.Component {
displayNameType={this.state.displayNameType}
displayPostsInCenter={this.state.displayPostsInCenter}
compactDisplay={this.state.compactDisplay}
previewsCollapsed={this.state.previewsCollapsed}
lastViewed={this.state.lastViewed}
/>
);

Просмотреть файл

@@ -234,13 +234,6 @@ export default class AdvancedSettingsDisplay extends React.Component {
defaultMessage='Show preview snippet of links below message'
/>
);
case 'EMBED_TOGGLE':
return (
<FormattedMessage
id='user.settings.advance.embed_toggle'
defaultMessage='Show toggle for all embed previews'
/>
);
default:
return null;
}

Просмотреть файл

@@ -24,7 +24,8 @@ function getDisplayStateFromStores() {
nameFormat: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'username'),
selectedFont: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'selected_font', Constants.DEFAULT_FONT),
channelDisplayMode: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT),
messageDisplay: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT)
messageDisplay: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT),
collapseDisplay: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.COLLAPSE_DISPLAY, Preferences.COLLAPSE_DISPLAY_DEFAULT)
};
}
@@ -41,9 +42,11 @@ export default class UserSettingsDisplay extends React.Component {
this.updateSection = this.updateSection.bind(this);
this.updateState = this.updateState.bind(this);
this.deactivate = this.deactivate.bind(this);
this.createCollapseSection = this.createCollapseSection.bind(this);
this.state = getDisplayStateFromStores();
}
handleSubmit() {
const userId = UserStore.getCurrentId();
@@ -77,8 +80,14 @@ export default class UserSettingsDisplay extends React.Component {
name: Preferences.MESSAGE_DISPLAY,
value: this.state.messageDisplay
};
const collapseDisplayPreference = {
user_id: userId,
category: Preferences.CATEGORY_DISPLAY_SETTINGS,
name: Preferences.COLLAPSE_DISPLAY,
value: this.state.collapseDisplay
};
AsyncClient.savePreferences([timePreference, namePreference, fontPreference, channelDisplayModePreference, messageDisplayPreference],
AsyncClient.savePreferences([timePreference, namePreference, fontPreference, channelDisplayModePreference, messageDisplayPreference, collapseDisplayPreference],
() => {
this.updateSection('');
},
@@ -87,27 +96,38 @@ export default class UserSettingsDisplay extends React.Component {
}
);
}
handleClockRadio(militaryTime) {
this.setState({militaryTime});
}
handleNameRadio(nameFormat) {
this.setState({nameFormat});
}
handleChannelDisplayModeRadio(channelDisplayMode) {
this.setState({channelDisplayMode});
}
handlemessageDisplayRadio(messageDisplay) {
this.setState({messageDisplay});
}
handleFont(selectedFont) {
Utils.applyFont(selectedFont);
this.setState({selectedFont});
}
handleCollapseRadio(collapseDisplay) {
this.setState({collapseDisplay});
}
updateSection(section) {
$('.settings-modal .modal-body').scrollTop(0).perfectScrollbar('update');
this.updateState();
this.props.updateSection(section);
}
updateState() {
const newState = getDisplayStateFromStores();
if (!Utils.areObjectsEqual(newState, this.state)) {
@@ -115,9 +135,118 @@ export default class UserSettingsDisplay extends React.Component {
this.setState(newState);
}
}
deactivate() {
this.updateState();
}
createCollapseSection() {
if (this.props.activeSection === 'collapse') {
const collapseFormat = [false, false];
if (this.state.collapseDisplay === 'true') {
collapseFormat[0] = true;
} else {
collapseFormat[1] = true;
}
const handleUpdateCollapseSection = (e) => {
this.updateSection('');
e.preventDefault();
};
const inputs = [
<div key='userDisplayCollapseOptions'>
<div className='radio'>
<label>
<input
type='radio'
name='collapseFormat'
checked={collapseFormat[0]}
onChange={this.handleCollapseRadio.bind(this, 'true')}
/>
<FormattedMessage
id='user.settings.display.collapseOn'
defaultMessage='On'
/>
</label>
<br/>
</div>
<div className='radio'>
<label>
<input
type='radio'
name='collapseFormat'
checked={collapseFormat[1]}
onChange={this.handleCollapseRadio.bind(this, 'false')}
/>
<FormattedMessage
id='user.settings.display.collapseOff'
defaultMessage='Off'
/>
</label>
<br/>
</div>
<div>
<br/>
<FormattedMessage
id='user.settings.display.collapseDesc'
defaultMessage='Toggle whether to automatically collapse all image previews.'
/>
</div>
</div>
];
return (
<SettingItemMax
title={
<FormattedMessage
id='user.settings.display.collapseDisplay'
defaultMessage='Auto Collapse Previews'
/>
}
inputs={inputs}
submit={this.handleSubmit}
server_error={this.state.serverError}
updateSection={handleUpdateCollapseSection}
/>
);
}
let describe;
if (this.state.collapseDisplay === 'true') {
describe = (
<FormattedMessage
id='user.settings.display.collapseOn'
defaultMessage='On'
/>
);
} else {
describe = (
<FormattedMessage
id='user.settings.display.collapseOff'
defaultMessage='Off'
/>
);
}
const handleUpdateCollapseSection = () => {
this.props.updateSection('collapse');
};
return (
<SettingItemMin
title={
<FormattedMessage
id='user.settings.display.collapseDisplay'
defaultMessage='Auto Collapse Previews'
/>
}
describe={describe}
updateSection={handleUpdateCollapseSection}
/>
);
}
render() {
const serverError = this.state.serverError || null;
let clockSection;
@@ -127,6 +256,8 @@ export default class UserSettingsDisplay extends React.Component {
let languagesSection;
let messageDisplaySection;
const collapseSection = this.createCollapseSection();
if (this.props.activeSection === 'clock') {
const clockFormat = [false, false];
if (this.state.militaryTime === 'true') {
@@ -729,6 +860,8 @@ export default class UserSettingsDisplay extends React.Component {
<div className='divider-dark'/>
{nameFormatSection}
<div className='divider-dark'/>
{collapseSection}
<div className='divider-dark'/>
{messageDisplaySection}
<div className='divider-dark'/>
{channelDisplayModeSection}

Просмотреть файл

@@ -1290,6 +1290,10 @@
"user.settings.developer.register": "Register New Application",
"user.settings.developer.thirdParty": "Open to register a new third-party application",
"user.settings.developer.title": "Developer Settings",
"user.settings.display.collapseOn": "On",
"user.settings.display.collapseOff": "Off",
"user.settings.display.collapseDesc": "Toggle whether to automatically collapse all image previews.",
"user.settings.display.collapseDisplay": "Auto Collapse Previews",
"user.settings.display.channelDisplayTitle": "Channel Display Mode",
"user.settings.display.channeldisplaymode": "Select the width of the center channel.",
"user.settings.display.clockDisplay": "Clock Display",

Просмотреть файл

@@ -82,8 +82,8 @@ class PreferenceStoreClass extends EventEmitter {
this.preferences.clear();
}
emitChange() {
this.emit(CHANGE_EVENT);
emitChange(category) {
this.emit(CHANGE_EVENT, category);
}
addChangeListener(callback) {
@@ -101,7 +101,7 @@ class PreferenceStoreClass extends EventEmitter {
case ActionTypes.RECEIVED_PREFERENCE: {
const preference = action.preference;
this.setPreference(preference.category, preference.name, preference.value);
this.emitChange();
this.emitChange(preference.category);
break;
}
case ActionTypes.RECEIVED_PREFERENCES:

Просмотреть файл

@@ -547,7 +547,9 @@ export default {
MESSAGE_DISPLAY: 'message_display',
MESSAGE_DISPLAY_CLEAN: 'clean',
MESSAGE_DISPLAY_COMPACT: 'compact',
MESSAGE_DISPLAY_DEFAULT: 'clean'
MESSAGE_DISPLAY_DEFAULT: 'clean',
COLLAPSE_DISPLAY: 'collapse_previews',
COLLAPSE_DISPLAY_DEFAULT: 'false'
},
TutorialSteps: {
INTRO_SCREENS: 0,
@@ -728,10 +730,6 @@ export default {
EMBED_PREVIEW: {
label: 'embed_preview',
description: 'Show preview snippet of links below message'
},
EMBED_TOGGLE: {
label: 'embed_toggle',
description: 'Show toggle for all embed previews'
}
},
OVERLAY_TIME_DELAY: 400,