* PLT-4167 Team Sidebar

* Address feedback from PM

* change route from my_members to members

* bug fixes

* Updating styles for teams sidebar (#4681)

* Added PM changes

* Fix corner cases

* Addressing feedback

* use two different endpoints

* Bug fixes

* Rename model and client functions, using preferences to store last team and channel viewed

* Fix mobile notification count and closing the team sidebar

* unit test, fixed bad merge and retrieve from cached when available

* bug fixes

* use id for last channel in preferences, query optimization

* Updating multi team css (#4830)
Этот коммит содержится в:
enahum
2016-12-19 10:05:46 -03:00
коммит произвёл Joram Wilander
родитель 3ce2ce9dc8
Коммит 999d1553e1
50 изменённых файлов: 1139 добавлений и 183 удалений

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

@@ -846,14 +846,21 @@ func setLastViewedAt(c *Context, w http.ResponseWriter, r *http.Request) {
Srv.Store.Channel().SetLastViewedAt(id, c.Session.UserId, newLastViewedAt)
preference := model.Preference{
chanPref := model.Preference{
UserId: c.Session.UserId,
Category: model.PREFERENCE_CATEGORY_LAST,
Category: c.TeamId,
Name: model.PREFERENCE_NAME_LAST_CHANNEL,
Value: id,
}
Srv.Store.Preference().Save(&model.Preferences{preference})
teamPref := model.Preference{
UserId: c.Session.UserId,
Category: model.PREFERENCE_CATEGORY_LAST,
Name: model.PREFERENCE_NAME_LAST_TEAM,
Value: c.TeamId,
}
Srv.Store.Preference().Save(&model.Preferences{teamPref, chanPref})
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CHANNEL_VIEWED, c.TeamId, "", c.Session.UserId, nil)
message.Add("channel_id", id)
@@ -901,14 +908,21 @@ func updateLastViewedAt(c *Context, w http.ResponseWriter, r *http.Request) {
go clearPushNotification(c.Session.UserId, id)
}
preference := model.Preference{
chanPref := model.Preference{
UserId: c.Session.UserId,
Category: model.PREFERENCE_CATEGORY_LAST,
Category: c.TeamId,
Name: model.PREFERENCE_NAME_LAST_CHANNEL,
Value: id,
}
Srv.Store.Preference().Save(&model.Preferences{preference})
teamPref := model.Preference{
UserId: c.Session.UserId,
Category: model.PREFERENCE_CATEGORY_LAST,
Name: model.PREFERENCE_NAME_LAST_TEAM,
Value: c.TeamId,
}
Srv.Store.Preference().Save(&model.Preferences{teamPref, chanPref})
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CHANNEL_VIEWED, c.TeamId, "", c.Session.UserId, nil)
message.Add("channel_id", id)

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

@@ -824,6 +824,7 @@ func sendNotifications(c *Context, post *model.Post, team *model.Team, channel *
message.Add("post", post.ToJson())
message.Add("channel_type", channel.Type)
message.Add("channel_display_name", channel.DisplayName)
message.Add("channel_name", channel.Name)
message.Add("sender_name", senderUsername)
message.Add("team_id", team.Id)

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

@@ -32,6 +32,8 @@ func InitTeam() {
BaseRoutes.Teams.Handle("/get_invite_info", ApiAppHandler(getInviteInfo)).Methods("POST")
BaseRoutes.Teams.Handle("/find_team_by_name", ApiAppHandler(findTeamByName)).Methods("POST")
BaseRoutes.Teams.Handle("/name/{team_name:[A-Za-z0-9\\-]+}", ApiAppHandler(getTeamByName)).Methods("GET")
BaseRoutes.Teams.Handle("/members", ApiUserRequired(getMyTeamMembers)).Methods("GET")
BaseRoutes.Teams.Handle("/unread", ApiUserRequired(getMyTeamsUnread)).Methods("GET")
BaseRoutes.NeedTeam.Handle("/me", ApiUserRequired(getMyTeam)).Methods("GET")
BaseRoutes.NeedTeam.Handle("/stats", ApiUserRequired(getTeamStats)).Methods("GET")
@@ -358,6 +360,11 @@ func LeaveTeam(team *model.Team, user *model.User) *model.AppError {
return uua.Err
}
// delete the preferences that set the last channel used in the team and other team specific preferences
if result := <-Srv.Store.Preference().DeleteCategory(user.Id, team.Id); result.Err != nil {
return result.Err
}
RemoveAllSessionsForUserId(user.Id)
InvalidateCacheForUser(user.Id)
@@ -723,6 +730,32 @@ func getTeamByName(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
func getMyTeamMembers(c *Context, w http.ResponseWriter, r *http.Request) {
if len(c.Session.TeamMembers) > 0 {
w.Write([]byte(model.TeamMembersToJson(c.Session.TeamMembers)))
} else {
if result := <-Srv.Store.Team().GetTeamsForUser(c.Session.UserId); result.Err != nil {
c.Err = result.Err
return
} else {
data := result.Data.([]*model.TeamMember)
w.Write([]byte(model.TeamMembersToJson(data)))
}
}
}
func getMyTeamsUnread(c *Context, w http.ResponseWriter, r *http.Request) {
teamId := r.URL.Query().Get("id")
if result := <-Srv.Store.Team().GetTeamsUnreadForUser(teamId, c.Session.UserId); result.Err != nil {
c.Err = result.Err
return
} else {
data := result.Data.([]*model.TeamUnread)
w.Write([]byte(model.TeamsUnreadToJson(data)))
}
}
func InviteMembers(team *model.Team, senderName string, invites []string) {
for _, invite := range invites {
if len(invite) > 0 {
@@ -801,6 +834,10 @@ func updateTeam(c *Context, w http.ResponseWriter, r *http.Request) {
oldTeam.Sanitize()
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_UPDATE_TEAM, "", "", "", nil)
message.Add("team", oldTeam.ToJson())
go Publish(message)
w.Write([]byte(oldTeam.ToJson()))
}

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

@@ -576,6 +576,41 @@ func TestGetTeamMembers(t *testing.T) {
}
}
func TestGetMyTeamMembers(t *testing.T) {
th := Setup().InitBasic()
if result, err := th.BasicClient.GetMyTeamMembers(); err != nil {
t.Fatal(err)
} else {
members := result.Data.([]*model.TeamMember)
if len(members) == 0 {
t.Fatal("should have results")
}
}
}
func TestGetMyTeamsUnread(t *testing.T) {
th := Setup().InitBasic()
if result, err := th.BasicClient.GetMyTeamsUnread(""); err != nil {
t.Fatal(err)
} else {
members := result.Data.([]*model.TeamUnread)
if len(members) == 0 {
t.Fatal("should have results")
}
}
if result, err := th.BasicClient.GetMyTeamsUnread(th.BasicTeam.Id); err != nil {
t.Fatal(err)
} else {
members := result.Data.([]*model.TeamUnread)
if len(members) != 0 {
t.Fatal("should not have results")
}
}
}
func TestGetTeamMember(t *testing.T) {
th := Setup().InitBasic()

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

@@ -4863,6 +4863,10 @@
"id": "store.sql_team.get_teams_for_email.app_error",
"translation": "We encountered a problem when looking up teams"
},
{
"id": "store.sql_team.get_unread.app_error",
"translation": "We couldn't get the teams unread messages"
},
{
"id": "store.sql_team.permanent_delete.app_error",
"translation": "We couldn't delete the existing team"

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

@@ -1752,6 +1752,36 @@ func (c *Client) GetTeamMembers(teamId string, offset int, limit int) (*Result,
}
}
// GetMyTeamMembers will return an array with team member objects that the current user
// is a member of. Must be authenticated.
func (c *Client) GetMyTeamMembers() (*Result, *AppError) {
if r, err := c.DoApiGet("/teams/members", "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), TeamMembersFromJson(r.Body)}, nil
}
}
// GetMyTeamsUnread will return an array with TeamUnread objects that contain the amount of
// unread messages and mentions the current user has for the teams it belongs to.
// An optional team ID can be set to exclude that team from the results. Must be authenticated.
func (c *Client) GetMyTeamsUnread(teamId string) (*Result, *AppError) {
endpoint := "/teams/unread"
if teamId != "" {
endpoint += fmt.Sprintf("?id=%s", url.QueryEscape(teamId))
}
if r, err := c.DoApiGet(endpoint, "", ""); err != nil {
return nil, err
} else {
defer closeBody(r)
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), TeamsUnreadFromJson(r.Body)}, nil
}
}
// GetTeamMember will return a team member object based on the team id and user id provided.
// Must be authenticated.
func (c *Client) GetTeamMember(teamId string, userId string) (*Result, *AppError) {

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

@@ -33,6 +33,7 @@ const (
PREFERENCE_CATEGORY_LAST = "last"
PREFERENCE_NAME_LAST_CHANNEL = "channel"
PREFERENCE_NAME_LAST_TEAM = "team"
PREFERENCE_CATEGORY_NOTIFICATIONS = "notifications"
PREFERENCE_NAME_EMAIL_INTERVAL = "email_interval"

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

@@ -16,6 +16,12 @@ type TeamMember struct {
DeleteAt int64 `json:"delete_at"`
}
type TeamUnread struct {
TeamId string `json:"team_id"`
MsgCount int64 `json:"msg_count"`
MentionCount int64 `json:"mention_count"`
}
func (o *TeamMember) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
@@ -55,6 +61,25 @@ func TeamMembersFromJson(data io.Reader) []*TeamMember {
}
}
func TeamsUnreadToJson(o []*TeamUnread) string {
if b, err := json.Marshal(o); err != nil {
return "[]"
} else {
return string(b)
}
}
func TeamsUnreadFromJson(data io.Reader) []*TeamUnread {
decoder := json.NewDecoder(data)
var o []*TeamUnread
err := decoder.Decode(&o)
if err == nil {
return o
} else {
return nil
}
}
func (o *TeamMember) IsValid() *AppError {
if len(o.TeamId) != 26 {

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

@@ -18,6 +18,7 @@ const (
WEBSOCKET_EVENT_DIRECT_ADDED = "direct_added"
WEBSOCKET_EVENT_NEW_USER = "new_user"
WEBSOCKET_EVENT_LEAVE_TEAM = "leave_team"
WEBSOCKET_EVENT_UPDATE_TEAM = "update_team"
WEBSOCKET_EVENT_USER_ADDED = "user_added"
WEBSOCKET_EVENT_USER_UPDATED = "user_updated"
WEBSOCKET_EVENT_USER_REMOVED = "user_removed"

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

@@ -326,3 +326,25 @@ func (s SqlPreferenceStore) Delete(userId, category, name string) StoreChannel {
return storeChannel
}
func (s SqlPreferenceStore) DeleteCategory(userId string, category string) StoreChannel {
storeChannel := make(StoreChannel, 1)
go func() {
result := StoreResult{}
if _, err := s.GetMaster().Exec(
`DELETE FROM
Preferences
WHERE
UserId = :UserId
AND Category = :Category`, map[string]interface{}{"UserId": userId, "Category": category}); err != nil {
result.Err = model.NewLocAppError("SqlPreferenceStore.DeleteCategory", "store.sql_preference.delete.app_error", nil, err.Error())
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}

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

@@ -392,3 +392,38 @@ func TestPreferenceDelete(t *testing.T) {
t.Fatal("should've returned no preferences")
}
}
func TestPreferenceDeleteCategory(t *testing.T) {
Setup()
category := model.NewId()
userId := model.NewId()
preference1 := model.Preference{
UserId: userId,
Category: category,
Name: model.NewId(),
Value: "value1a",
}
preference2 := model.Preference{
UserId: userId,
Category: category,
Name: model.NewId(),
Value: "value1a",
}
Must(store.Preference().Save(&model.Preferences{preference1, preference2}))
if prefs := Must(store.Preference().GetAll(userId)).(model.Preferences); len([]model.Preference(prefs)) != 2 {
t.Fatal("should've returned 2 preferences")
}
if result := <-store.Preference().DeleteCategory(userId, category); result.Err != nil {
t.Fatal(result.Err)
}
if prefs := Must(store.Preference().GetAll(userId)).(model.Preferences); len([]model.Preference(prefs)) != 0 {
t.Fatal("should've returned no preferences")
}
}

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

@@ -583,6 +583,39 @@ func (s SqlTeamStore) GetTeamsForUser(userId string) StoreChannel {
return storeChannel
}
func (s SqlTeamStore) GetTeamsUnreadForUser(teamId, userId string) StoreChannel {
storeChannel := make(StoreChannel, 1)
go func() {
result := StoreResult{}
var members []*model.TeamUnread
_, err := s.GetReplica().Select(&members,
`SELECT
Channels.TeamId,
SUM(Channels.TotalMsgCount - ChannelMembers.MsgCount) as MsgCount,
SUM(ChannelMembers.MentionCount) as MentionCount
FROM
Channels,
ChannelMembers
WHERE
Channels.Id = ChannelMembers.ChannelId AND
ChannelMembers.UserId = :UserId AND Channels.TeamId != :TeamId
GROUP BY
Channels.TeamId`, map[string]interface{}{"UserId": userId, "TeamId": teamId})
if err != nil {
result.Err = model.NewLocAppError("SqlTeamStore.GetTeamsUnreadForUser", "store.sql_team.get_unread.app_error", nil, "userId="+userId+" "+err.Error())
} else {
result.Data = members
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}
func (s SqlTeamStore) RemoveMember(teamId string, userId string) StoreChannel {
storeChannel := make(StoreChannel, 1)

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

@@ -529,3 +529,50 @@ func TestTeamStoreMemberCount(t *testing.T) {
}
}
}
func TestMyTeamMembersUnread(t *testing.T) {
Setup()
teamId1 := model.NewId()
teamId2 := model.NewId()
uid := model.NewId()
m1 := &model.TeamMember{TeamId: teamId1, UserId: uid}
m2 := &model.TeamMember{TeamId: teamId2, UserId: uid}
Must(store.Team().SaveMember(m1))
Must(store.Team().SaveMember(m2))
c1 := &model.Channel{TeamId: m1.TeamId, Name: model.NewId(), DisplayName: "Town Square", Type: model.CHANNEL_OPEN}
Must(store.Channel().Save(c1))
c2 := &model.Channel{TeamId: m2.TeamId, Name: model.NewId(), DisplayName: "Town Square", Type: model.CHANNEL_OPEN}
Must(store.Channel().Save(c2))
cm1 := &model.ChannelMember{ChannelId: c1.Id, UserId: m1.UserId, NotifyProps: model.GetDefaultChannelNotifyProps()}
Must(store.Channel().SaveMember(cm1))
cm2 := &model.ChannelMember{ChannelId: c2.Id, UserId: m2.UserId, NotifyProps: model.GetDefaultChannelNotifyProps()}
Must(store.Channel().SaveMember(cm2))
if r1 := <-store.Team().GetTeamsUnreadForUser("", uid); r1.Err != nil {
t.Fatal(r1.Err)
} else {
ms := r1.Data.([]*model.TeamUnread)
if len(ms) != 2 {
t.Fatal("Should be the unreads for all the teams")
}
}
if r2 := <-store.Team().GetTeamsUnreadForUser(teamId1, uid); r2.Err != nil {
t.Fatal(r2.Err)
} else {
ms := r2.Data.([]*model.TeamUnread)
if len(ms) != 1 {
t.Fatal("Should be the unreads for just one team")
}
}
if r1 := <-store.Team().RemoveAllMembersByUser(uid); r1.Err != nil {
t.Fatal(r1.Err)
}
}

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

@@ -74,6 +74,7 @@ type TeamStore interface {
GetTotalMemberCount(teamId string) StoreChannel
GetActiveMemberCount(teamId string) StoreChannel
GetTeamsForUser(userId string) StoreChannel
GetTeamsUnreadForUser(teamId, userId string) StoreChannel
RemoveMember(teamId string, userId string) StoreChannel
RemoveAllMembersByTeam(teamId string) StoreChannel
RemoveAllMembersByUser(userId string) StoreChannel
@@ -270,6 +271,7 @@ type PreferenceStore interface {
GetCategory(userId string, category string) StoreChannel
GetAll(userId string) StoreChannel
Delete(userId, category, name string) StoreChannel
DeleteCategory(userId string, category string) StoreChannel
PermanentDeleteByUser(userId string) StoreChannel
IsFeatureEnabled(feature, userId string) StoreChannel
}

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

@@ -43,6 +43,7 @@ export function emitChannelClickEvent(channel) {
);
}
function switchToChannel(chan) {
const channelMember = ChannelStore.getMyMember(chan.id);
const getMyChannelMembersPromise = AsyncClient.getChannelMember(chan.id, UserStore.getCurrentId());
getMyChannelMembersPromise.then(() => {
@@ -56,6 +57,9 @@ export function emitChannelClickEvent(channel) {
type: ActionTypes.CLICK_CHANNEL,
name: chan.name,
id: chan.id,
team_id: chan.team_id,
total_msg_count: chan.total_msg_count,
channelMember,
prev: ChannelStore.getCurrentId()
});
}
@@ -443,7 +447,7 @@ export function viewLoggedIn() {
PostStore.clearPendingPosts();
}
var lastTimeTypingSent = 0;
let lastTimeTypingSent = 0;
export function emitLocalUserTypingEvent(channelId, parentId) {
const t = Date.now();
if ((t - lastTimeTypingSent) > Constants.UPDATE_TYPING_MS) {
@@ -534,3 +538,35 @@ export function emitBrowserFocus(focus) {
focus
});
}
export function redirectUserToDefaultTeam() {
const teams = TeamStore.getAll();
const teamMembers = TeamStore.getMyTeamMembers();
let teamId = PreferenceStore.get('last', 'team');
if (!teams[teamId] && teamMembers.length > 0) {
let myTeams = [];
for (const index in teamMembers) {
if (teamMembers.hasOwnProperty(index)) {
const teamMember = teamMembers[index];
myTeams.push(teams[teamMember.team_id]);
}
}
if (myTeams.length > 0) {
myTeams = myTeams.sort((a, b) => a.display_name.localeCompare(b.display_name));
teamId = myTeams[0].id;
}
}
if (teams[teamId]) {
const channelId = PreferenceStore.get(teamId, 'channel');
let channel = ChannelStore.getChannelById(channelId);
if (!channel) {
channel = 'town-square';
}
browserHistory.push(`/${teams[teamId].name}/channels/${channel}`);
} else {
browserHistory.push('/select_team');
}
}

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

@@ -18,23 +18,30 @@ const ActionTypes = Constants.ActionTypes;
const Preferences = Constants.Preferences;
export function handleNewPost(post, msg) {
const teamId = TeamStore.getCurrentId();
if (ChannelStore.getCurrentId() === post.channel_id) {
if (window.isActive) {
AsyncClient.updateLastViewedAt(null, false);
} else {
AsyncClient.getChannel(post.channel_id);
}
} else if (msg && (TeamStore.getCurrentId() === msg.data.team_id || msg.data.channel_type === Constants.DM_CHANNEL)) {
} else if (msg && (teamId === msg.data.team_id || msg.data.channel_type === Constants.DM_CHANNEL)) {
if (Client.teamId) {
AsyncClient.getChannel(post.channel_id);
}
}
var websocketMessageProps = null;
let websocketMessageProps = null;
if (msg) {
websocketMessageProps = msg.data;
}
const myTeams = TeamStore.getMyTeamMembers();
if (msg.data.team_id !== teamId && myTeams.filter((m) => m.team_id === msg.data.team_id).length) {
AsyncClient.getMyTeamsUnread(teamId);
}
if (post.root_id && PostStore.getPost(post.channel_id, post.root_id) == null) {
Client.getPost(
post.channel_id,

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

@@ -6,6 +6,7 @@ import $ from 'jquery';
import UserStore from 'stores/user_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import PostStore from 'stores/post_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import BrowserStore from 'stores/browser_store.jsx';
import ErrorStore from 'stores/error_store.jsx';
@@ -122,6 +123,10 @@ function handleEvent(msg) {
handleLeaveTeamEvent(msg);
break;
case SocketEvents.UPDATE_TEAM:
handleUpdateTeamEvent(msg);
break;
case SocketEvents.USER_ADDED:
handleUserAddedEvent(msg);
break;
@@ -229,21 +234,37 @@ function handleNewUserEvent(msg) {
AsyncClient.getUser(msg.data.user_id);
AsyncClient.getChannelStats();
loadProfilesAndTeamMembersForDMSidebar();
if (msg.data.user_id === UserStore.getCurrentId()) {
AsyncClient.getMyTeamMembers();
}
}
function handleLeaveTeamEvent(msg) {
if (UserStore.getCurrentId() === msg.data.user_id) {
TeamStore.removeMyTeamMember(msg.data.team_id);
// if they are on the team being removed redirect them to the root
// if they are on the team being removed redirect them to default team
if (TeamStore.getCurrentId() === msg.data.team_id) {
TeamStore.setCurrentId('');
Client.setTeamId('');
browserHistory.push('/');
PreferenceStore.deletePreference({
category: 'last',
name: 'team',
value: msg.data.team_id
});
GlobalActions.redirectUserToDefaultTeam();
}
} else {
UserStore.removeProfileFromTeam(msg.data.team_id, msg.data.user_id);
TeamStore.removeMemberInTeam(msg.data.team_id, msg.data.user_id);
}
}
function handleUpdateTeamEvent(msg) {
TeamStore.updateTeam(msg.data.team);
}
function handleDirectAddedEvent(msg) {
AsyncClient.getChannel(msg.broadcast.channel_id);
loadProfilesAndTeamMembersForDMSidebar();

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

@@ -598,6 +598,30 @@ export default class Client {
end(this.handleResponse.bind(this, 'getTeamMember', success, error));
}
getMyTeamMembers(success, error) {
request.
get(`${this.getTeamsRoute()}/members`).
set(this.defaultHeaders).
type('application/json').
accept('application/json').
end(this.handleResponse.bind(this, 'getMyTeamMembers', success, error));
}
getMyTeamsUnread(teamId, success, error) {
let url = `${this.getTeamsRoute()}/unread`;
if (teamId) {
url += `?id=${encodeURIComponent(teamId)}`;
}
request.
get(url).
set(this.defaultHeaders).
type('application/json').
accept('application/json').
end(this.handleResponse.bind(this, 'getMyTeamsUnread', success, error));
}
getTeamMembersByIds(teamId, userIds, success, error) {
request.
post(`${this.getTeamNeededRoute(teamId)}/members/ids`).

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

@@ -50,13 +50,14 @@ export default class AdminNavbarDropdown extends React.Component {
}
render() {
var teams = [];
const teams = [];
let switchTeams;
if (this.state.teamMembers && this.state.teamMembers.length > 0) {
for (var index in this.state.teamMembers) {
for (const index in this.state.teamMembers) {
if (this.state.teamMembers.hasOwnProperty(index)) {
var teamMember = this.state.teamMembers[index];
var team = this.state.teams[teamMember.team_id];
const teamMember = this.state.teamMembers[index];
const team = this.state.teams[teamMember.team_id];
teams.push(
<li key={'team_' + team.name}>
<Link
@@ -79,6 +80,20 @@ export default class AdminNavbarDropdown extends React.Component {
className='divider'
/>
);
} else {
switchTeams = (
<li>
<Link
to={'/select_team'}
>
<i className='fa fa-exchange'/>
<FormattedMessage
id='admin.nav.switch'
defaultMessage='Team Selection'
/>
</Link>
</li>
);
}
return (
@@ -104,17 +119,7 @@ export default class AdminNavbarDropdown extends React.Component {
role='menu'
>
{teams}
<li>
<Link
to={'/select_team'}
>
<i className='fa fa-exchange'/>
<FormattedMessage
id='admin.nav.switch'
defaultMessage='Team Selection'
/>
</Link>
</li>
{switchTeams}
<li
key='teamDiv'
className='divider'

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

@@ -2,6 +2,8 @@
// See License.txt for license information.
import ErrorBar from 'components/error_bar.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import {FormattedMessage} from 'react-intl';
import {browserHistory, Link} from 'react-router/es6';
@@ -47,11 +49,21 @@ export default class CreateTeamController extends React.Component {
);
}
let url = '/select_team';
const team = TeamStore.getCurrent();
const channel = ChannelStore.getCurrent();
if (team) {
url = `/${team.name}`;
if (channel) {
url += `/channels/${channel.name}`;
}
}
return (
<div>
<ErrorBar/>
<div className='signup-header'>
<Link to='/select_team'>
<Link to={url}>
<span className='fa fa-chevron-left'/>
<FormattedMessage
id='web.header.back'

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

@@ -54,7 +54,7 @@ export default class LoginController extends React.Component {
document.title = global.window.mm_config.SiteName;
if (UserStore.getCurrentUser()) {
browserHistory.push('/select_team');
GlobalActions.redirectUserToDefaultTeam();
}
AsyncClient.checkVersion();
@@ -205,7 +205,7 @@ export default class LoginController extends React.Component {
} else if (team) {
browserHistory.push(`/${team.name}`);
} else {
browserHistory.push('/select_team');
GlobalActions.redirectUserToDefaultTeam();
}
}
);

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

@@ -146,6 +146,7 @@ export default class Navbar extends React.Component {
if (e.target.className !== 'navbar-toggle' && e.target.className !== 'icon-bar') {
$('.app__body .inner-wrap').removeClass('move--right move--left move--left-small');
$('.app__body .sidebar--left').removeClass('move--right');
$('.multi-teams .team-sidebar').removeClass('move--right');
$('.app__body .sidebar--right').removeClass('move--left');
$('.app__body .sidebar--menu').removeClass('move--left');
}
@@ -155,6 +156,7 @@ export default class Navbar extends React.Component {
toggleLeftSidebar() {
$('.app__body .inner-wrap').toggleClass('move--right');
$('.app__body .sidebar--left').toggleClass('move--right');
$('.multi-teams .team-sidebar').toggleClass('move--right');
}
toggleRightSidebar() {

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

@@ -141,6 +141,7 @@ export default class NeedsTeam extends React.Component {
content.push(
this.props.navbar
);
content.push(this.props.team_sidebar);
content.push(
this.props.sidebar
);
@@ -197,6 +198,7 @@ NeedsTeam.propTypes = {
]),
navbar: React.PropTypes.element,
sidebar: React.PropTypes.element,
team_sidebar: React.PropTypes.element,
center: React.PropTypes.element,
params: React.PropTypes.object,
user: React.PropTypes.object

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

@@ -3,14 +3,22 @@
import * as utils from 'utils/utils.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import TeamStore from 'stores/team_store.jsx';
function getCountsStateFromStores() {
var count = 0;
var channels = ChannelStore.getAll();
var members = ChannelStore.getMyMembers();
let count = 0;
const teamMembers = TeamStore.getMyTeamMembers();
const channels = ChannelStore.getAll();
const members = ChannelStore.getMyMembers();
teamMembers.forEach((member) => {
if (member.team_id !== TeamStore.getCurrentId()) {
count += ((member.msg_count || 0) + (member.mention_count || 0));
}
});
channels.forEach((channel) => {
var channelMember = members[channel.id];
const channelMember = members[channel.id];
if (channelMember == null) {
return;
}
@@ -41,10 +49,12 @@ export default class NotifyCounts extends React.Component {
componentDidMount() {
this.mounted = true;
ChannelStore.addChangeListener(this.onListenerChange);
TeamStore.addChangeListener(this.onListenerChange);
}
componentWillUnmount() {
this.mounted = false;
ChannelStore.removeChangeListener(this.onListenerChange);
TeamStore.removeChangeListener(this.onListenerChange);
}
onListenerChange() {
if (this.mounted) {

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

@@ -50,7 +50,7 @@ export default class Root extends React.Component {
if (UserStore.getNoAccounts()) {
browserHistory.push('/signup_user_complete');
} else if (UserStore.getCurrentUser()) {
browserHistory.push('/select_team');
GlobalActions.redirectUserToDefaultTeam();
} else {
browserHistory.push('/login');
}

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

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

@@ -24,6 +24,7 @@ export default class SelectTeam extends React.Component {
super(props);
this.onTeamChange = this.onTeamChange.bind(this);
this.handleTeamClick = this.handleTeamClick.bind(this);
this.teamContentsCompare = this.teamContentsCompare.bind(this);
const state = this.getStateFromStores(false);
state.loadingTeamId = '';
@@ -61,33 +62,15 @@ export default class SelectTeam extends React.Component {
}
render() {
let content = null;
let teamContents = [];
let openTeamContents = [];
const isAlreadyMember = new Map();
const isSystemAdmin = Utils.isSystemAdmin(UserStore.getCurrentUser().roles);
let teamMembersCount = 0;
for (const teamMember of this.state.teamMembers) {
const teamId = teamMember.team_id;
const team = this.state.teams[teamId];
isAlreadyMember[teamId] = true;
teamMembersCount++;
teamContents.push(
<SelectTeamItem
key={'team_' + team.name}
team={team}
url={'/' + team.name + '/channels/town-square'}
onTeamClick={this.handleTeamClick}
loading={this.state.loadingTeamId === teamId}
/>
);
}
teamContents = teamContents.sort(this.teamContentsCompare);
var openTeamContents = [];
for (const id in this.state.teamListings) {
if (this.state.teamListings.hasOwnProperty(id) && !isAlreadyMember[id]) {
const openTeam = this.state.teamListings[id];
@@ -103,8 +86,8 @@ export default class SelectTeam extends React.Component {
}
}
if (teamMembersCount === 0 && teamContents.length === 0 && openTeamContents.length === 0 && (global.window.mm_config.EnableTeamCreation === 'true' || isSystemAdmin)) {
teamContents = (
if (openTeamContents.length === 0 && (global.window.mm_config.EnableTeamCreation === 'true' || isSystemAdmin)) {
openTeamContents = (
<div className='signup-team-dir-err'>
<div>
<FormattedMessage
@@ -114,8 +97,8 @@ export default class SelectTeam extends React.Component {
</div>
</div>
);
} else if (teamMembersCount === 0 && teamContents.length === 0 && openTeamContents.length === 0) {
teamContents = (
} else if (openTeamContents.length === 0) {
openTeamContents = (
<div className='signup-team-dir-err'>
<div>
<FormattedMessage
@@ -125,42 +108,25 @@ export default class SelectTeam extends React.Component {
</div>
</div>
);
} else if (teamContents.length === 0 && openTeamContents.length > 0) {
teamContents = null;
}
if (teamContents) {
content = (
<div className='signup__content'>
<h4>
<FormattedMessage
id='signup_team.choose'
defaultMessage='Your teams:'
/>
</h4>
<div className='signup-team-all'>
{teamContents}
</div>
</div>
);
if (Array.isArray(openTeamContents)) {
openTeamContents = openTeamContents.sort(this.teamContentsCompare);
}
var openContent;
if (openTeamContents.length > 0) {
openContent = (
<div className='signup__content'>
<h4>
<FormattedMessage
id='signup_team.join_open'
defaultMessage='Teams you can join: '
/>
</h4>
<div className='signup-team-all'>
{openTeamContents.sort(this.teamContentsCompare)}
</div>
let openContent = (
<div className='signup__content'>
<h4>
<FormattedMessage
id='signup_team.join_open'
defaultMessage='Teams you can join: '
/>
</h4>
<div className='signup-team-all'>
{openTeamContents}
</div>
);
}
</div>
);
if (!this.state.loaded) {
openContent = <LoadingScreen/>;
@@ -177,7 +143,7 @@ export default class SelectTeam extends React.Component {
}
let teamSignUp;
if (isSystemAdmin || (global.window.mm_config.EnableTeamCreation === 'true' && !UserAgent.isMobileApp())) {
if (isSystemAdmin || global.window.mm_config.EnableTeamCreation === 'true') {
teamSignUp = (
<div className='margin--extra'>
<Link
@@ -197,7 +163,7 @@ export default class SelectTeam extends React.Component {
}
let adminConsoleLink;
if (isSystemAdmin) {
if (isSystemAdmin && !UserAgent.isMobileApp()) {
adminConsoleLink = (
<div className='margin--extra hidden-xs'>
<Link
@@ -225,19 +191,30 @@ export default class SelectTeam extends React.Component {
);
}
let headerButton;
if (this.state.teamMembers.length) {
headerButton = (
<Link to='/'>
<span className='fa fa-chevron-left'/>
<FormattedMessage id='web.header.back'/>
</Link>
);
} else {
headerButton = (
<a
href='#'
onClick={GlobalActions.emitUserLoggedOutEvent}
>
<span className='fa fa-chevron-left'/>
<FormattedMessage id='web.header.logout'/>
</a>
);
}
return (
<div>
<ErrorBar/>
<div className='signup-header'>
<a
href='#'
onClick={GlobalActions.emitUserLoggedOutEvent}
>
<span className='fa fa-chevron-left'/>
<FormattedMessage
id='navbar_dropdown.logout'
/>
</a>
{headerButton}
</div>
<div className='col-sm-12'>
<div className={'signup-team__container'}>
@@ -249,7 +226,6 @@ export default class SelectTeam extends React.Component {
<h4 className='color--light'>
{description}
</h4>
{content}
{openContent}
{teamSignUp}
{adminConsoleLink}

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

@@ -82,11 +82,19 @@ export default class Sidebar extends React.Component {
let msgs = 0;
let mentions = 0;
const unreadCounts = this.state.unreadCounts;
const teamMembers = this.state.teamMembers;
teamMembers.forEach((member) => {
if (member.team_id !== this.state.currentTeam.id) {
msgs += member.msg_count || 0;
mentions += member.mention_count || 0;
}
});
Object.keys(unreadCounts).forEach((chId) => {
const channel = ChannelStore.get(chId);
if (channel && (!channel.team_id || channel.team_id === this.state.currentTeam.id)) {
if (channel && (channel.type === 'D' || channel.team_id === this.state.currentTeam.id)) {
msgs += unreadCounts[chId].msgs;
mentions += unreadCounts[chId].mentions;
}
@@ -97,6 +105,7 @@ export default class Sidebar extends React.Component {
getStateFromStores() {
const members = ChannelStore.getMyMembers();
const teamMembers = TeamStore.getMyTeamMembers();
const currentChannelId = ChannelStore.getCurrentId();
const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999);
const channelList = ChannelUtils.buildDisplayableChannelList(Object.assign([], ChannelStore.getAll()));
@@ -104,6 +113,7 @@ export default class Sidebar extends React.Component {
return {
activeId: currentChannelId,
members,
teamMembers,
...channelList,
unreadCounts: JSON.parse(JSON.stringify(ChannelStore.getUnreadCounts())),
showTutorialTip: tutorialStep === TutorialSteps.CHANNEL_POPOVER,
@@ -153,6 +163,7 @@ export default class Sidebar extends React.Component {
if (this.state.activeId !== prevState.activeId) {
$('.app__body .inner-wrap').removeClass('move--right');
$('.app__body .sidebar--left').removeClass('move--right');
$('.multi-teams .team-sidebar').removeClass('move--right');
}
}
@@ -169,6 +180,7 @@ export default class Sidebar extends React.Component {
onChange() {
this.setState(this.getStateFromStores());
this.updateTitle();
}
updateTitle() {

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

@@ -45,7 +45,6 @@ export default class SidebarHeaderDropdown extends React.Component {
this.showGetTeamInviteLinkModal = this.showGetTeamInviteLinkModal.bind(this);
this.showTeamMembersModal = this.showTeamMembersModal.bind(this);
this.hideTeamMembersModal = this.hideTeamMembersModal.bind(this);
this.handleSwitchTeams = this.handleSwitchTeams.bind(this);
this.onTeamChange = this.onTeamChange.bind(this);
this.openAccountSettings = this.openAccountSettings.bind(this);
@@ -55,8 +54,8 @@ export default class SidebarHeaderDropdown extends React.Component {
this.handleClick = this.handleClick.bind(this);
this.state = {
teams: TeamStore.getAll(),
teamMembers: TeamStore.getMyTeamMembers(),
teamListings: TeamStore.getTeamListings(),
showAboutModal: false,
showDropdown: false,
showTeamMembersModal: false,
@@ -131,11 +130,6 @@ export default class SidebarHeaderDropdown extends React.Component {
});
}
handleSwitchTeams() {
// The actual switching of teams is handled by the react-router Link
this.setState({showDropdown: false});
}
componentDidMount() {
TeamStore.addChangeListener(this.onTeamChange);
document.addEventListener('keydown', this.openAccountSettings);
@@ -143,8 +137,8 @@ export default class SidebarHeaderDropdown extends React.Component {
onTeamChange() {
this.setState({
teams: TeamStore.getAll(),
teamMembers: TeamStore.getMyTeamMembers()
teamMembers: TeamStore.getMyTeamMembers(),
teamListings: TeamStore.getTeamListings()
});
}
@@ -182,14 +176,14 @@ export default class SidebarHeaderDropdown extends React.Component {
render() {
const config = global.mm_config;
var teamLink = '';
var inviteLink = '';
var manageLink = '';
var sysAdminLink = '';
var currentUser = this.props.currentUser;
var isAdmin = false;
var isSystemAdmin = false;
var teamSettings = null;
const currentUser = this.props.currentUser;
let teamLink = '';
let inviteLink = '';
let manageLink = '';
let sysAdminLink = '';
let isAdmin = false;
let isSystemAdmin = false;
let teamSettings = null;
let integrationsLink = null;
if (!currentUser) {
@@ -322,6 +316,7 @@ export default class SidebarHeaderDropdown extends React.Component {
}
const teams = [];
let moreTeams = false;
if (config.EnableTeamCreation === 'true') {
teams.push(
@@ -340,6 +335,31 @@ export default class SidebarHeaderDropdown extends React.Component {
);
}
const isAlreadyMember = this.state.teamMembers.reduce((result, item) => {
result[item.team_id] = true;
return result;
}, {});
for (const id in this.state.teamListings) {
if (this.state.teamListings.hasOwnProperty(id) && !isAlreadyMember[id]) {
moreTeams = true;
break;
}
}
if (moreTeams) {
teams.push(
<li key='joinTeam_li'>
<Link to='/select_team'>
<FormattedMessage
id='navbar_dropdown.join'
defaultMessage='Join Another Team'
/>
</Link>
</li>
);
}
teams.push(
<li key='leaveTeam_li'>
<a
@@ -354,39 +374,6 @@ export default class SidebarHeaderDropdown extends React.Component {
</li>
);
if (this.state.teamMembers && this.state.teamMembers.length > 1) {
teams.push(
<li
key='teamDiv'
className='divider'
/>
);
for (var index in this.state.teamMembers) {
if (this.state.teamMembers.hasOwnProperty(index)) {
var teamMember = this.state.teamMembers[index];
var team = this.state.teams[teamMember.team_id];
if (team.name !== this.props.teamName) {
teams.push(
<li key={'team_' + team.name}>
<Link
to={'/' + team.name + '/channels/town-square'}
onClick={this.handleSwitchTeams}
>
<FormattedMessage
id='navbar_dropdown.switchTo'
defaultMessage='Switch to '
/>
{team.display_name}
</Link>
</li>
);
}
}
}
}
let helpLink = null;
if (config.HelpLink) {
helpLink = (

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

@@ -82,6 +82,7 @@ export default class SidebarRight extends React.Component {
$('.app__body .inner-wrap').removeClass('.move--right');
$('.app__body .inner-wrap').addClass('move--left');
$('.app__body .sidebar--left').removeClass('move--right');
$('.multi-teams .team-sidebar').removeClass('move--right');
$('.app__body .sidebar--right').addClass('move--left');
//$('.sidebar--right').prepend('<div class="sidebar__overlay"></div>');

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

@@ -391,15 +391,6 @@ export default class SidebarRightMenu extends React.Component {
{teamSettingsLink}
{manageLink}
{consoleLink}
<li>
<Link to='/select_team'>
<i className='icon fa fa-exchange'/>
<FormattedMessage
id='sidebar_right_menu.switch_team'
defaultMessage='Team Selection'
/>
</Link>
</li>
<li className='divider'/>
{helpLink}
{reportLink}

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

@@ -110,7 +110,7 @@ export default class SignupEmail extends React.Component {
if (query.redirect_to) {
browserHistory.push(query.redirect_to);
} else {
browserHistory.push('/select_team');
GlobalActions.redirectUserToDefaultTeam();
}
}
);
@@ -133,7 +133,7 @@ export default class SignupEmail extends React.Component {
if (query.redirect_to) {
browserHistory.push(query.redirect_to);
} else {
browserHistory.push('/select_team');
GlobalActions.redirectUserToDefaultTeam();
}
}
);

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

@@ -90,8 +90,13 @@ export default class SignupLdap extends React.Component {
finishSignup() {
GlobalActions.emitInitialLoad(
() => {
const query = this.props.location.query;
GlobalActions.loadDefaultLocale();
browserHistory.push('/select_team');
if (query.redirect_to) {
browserHistory.push(query.redirect_to);
} else {
GlobalActions.redirectUserToDefaultTeam();
}
}
);
}

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

@@ -122,7 +122,7 @@ export default class SignupController extends React.Component {
}
if (userLoggedIn) {
browserHistory.push('/select_team');
GlobalActions.redirectUserToDefaultTeam();
}
}
}

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

@@ -0,0 +1,84 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import Constants from 'utils/constants.jsx';
import React from 'react';
import {Link} from 'react-router/es6';
import {Tooltip, OverlayTrigger} from 'react-bootstrap';
export default class TeamButton extends React.Component {
constructor(props) {
super(props);
this.handleDisabled = this.handleDisabled.bind(this);
}
handleDisabled(e) {
e.preventDefault();
}
render() {
let teamClass = this.props.active ? 'active' : '';
const disabled = this.props.disabled ? 'team-disabled' : '';
const handleClick = (this.props.active || this.props.disabled) ? this.handleDisabled : null;
let badge;
if (!teamClass) {
teamClass = this.props.unread ? 'unread' : '';
if (this.props.mentions) {
badge = (
<span className='badge pull-right small'>{this.props.mentions}</span>
);
}
}
return (
<div
className={`team-container ${teamClass}`}
>
<Link
className={disabled}
to={this.props.url}
onClick={handleClick}
>
<OverlayTrigger
delayShow={Constants.OVERLAY_TIME_DELAY}
placement={this.props.placement}
overlay={
<Tooltip id={`tooltip-${this.props.url}`}>
{this.props.tip}
</Tooltip>
}
>
<div className='team-btn'>
{badge}
{this.props.contents}
</div>
</OverlayTrigger>
</Link>
</div>
);
}
}
TeamButton.defaultProps = {
tip: '',
placement: 'right',
active: false,
disabled: false,
unread: false,
mentions: 0
};
TeamButton.propTypes = {
url: React.PropTypes.string.isRequired,
contents: React.PropTypes.node.isRequired,
tip: React.PropTypes.node,
active: React.PropTypes.bool,
disabled: React.PropTypes.bool,
unread: React.PropTypes.bool,
mentions: React.PropTypes.number,
placement: React.PropTypes.oneOf(['left', 'right', 'top', 'bottom'])
};

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

@@ -0,0 +1,169 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import TeamButton from './components/team_button.jsx';
import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import * as Utils from 'utils/utils.jsx';
import $ from 'jquery';
import React from 'react';
import {FormattedMessage} from 'react-intl';
export default class TeamSidebar extends React.Component {
constructor(props) {
super(props);
this.getStateFromStores = this.getStateFromStores.bind(this);
this.onChange = this.onChange.bind(this);
this.handleResize = this.handleResize.bind(this);
this.setStyles = this.setStyles.bind(this);
this.state = this.getStateFromStores();
}
getStateFromStores() {
const teamMembers = TeamStore.getMyTeamMembers();
const currentTeamId = TeamStore.getCurrentId();
return {
teams: TeamStore.getAll(),
teamListings: TeamStore.getTeamListings(),
teamMembers,
currentTeamId,
show: teamMembers && teamMembers.length > 1
};
}
componentDidMount() {
window.addEventListener('resize', this.handleResize);
TeamStore.addChangeListener(this.onChange);
TeamStore.addUnreadChangeListener(this.onChange);
AsyncClient.getAllTeamListings();
this.setStyles();
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
TeamStore.removeChangeListener(this.onChange);
TeamStore.removeUnreadChangeListener(this.onChange);
}
componentDidUpdate(prevProps, prevState) {
$('.team-wrapper').perfectScrollbar();
// reset the scrollbar upon switching teams
if (this.state.currentTeam !== prevState.currentTeam) {
this.refs.container.scrollTop = 0;
$('.team-wrapper').perfectScrollbar('update');
}
}
onChange() {
this.setState(this.getStateFromStores());
this.setStyles();
}
handleResize() {
const teamMembers = this.state.teamMembers;
this.setState({show: teamMembers && teamMembers.length > 1});
this.setStyles();
}
setStyles() {
const root = document.querySelector('#root');
if (this.state.show) {
root.classList.add('multi-teams');
} else {
root.classList.remove('multi-teams');
}
}
render() {
if (!this.state.show) {
return null;
}
const myTeams = [];
const isSystemAdmin = Utils.isSystemAdmin(UserStore.getCurrentUser().roles);
const isAlreadyMember = new Map();
let moreTeams = false;
for (const index in this.state.teamMembers) {
if (this.state.teamMembers.hasOwnProperty(index)) {
const teamMember = this.state.teamMembers[index];
const teamId = teamMember.team_id;
myTeams.push(Object.assign({
unread: teamMember.msg_count > 0,
mentions: teamMember.mention_count
}, this.state.teams[teamId]));
isAlreadyMember[teamId] = true;
}
}
for (const id in this.state.teamListings) {
if (this.state.teamListings.hasOwnProperty(id) && !isAlreadyMember[id]) {
moreTeams = true;
break;
}
}
const teams = myTeams.
sort((a, b) => a.display_name.localeCompare(b.display_name)).
map((team) => {
return (
<TeamButton
key={'switch_team_' + team.name}
url={`/${team.name}`}
tip={team.display_name}
active={team.id === this.state.currentTeamId}
contents={team.display_name.substring(0, 1).toUpperCase()}
unread={team.unread}
mentions={team.mentions}
/>
);
});
if (moreTeams) {
teams.push(
<TeamButton
key='more_teams'
url='/select_team'
tip={
<FormattedMessage
id='team_sidebar.join'
defaultMessage='Other teams you can join.'
/>
}
contents={<i className='fa fa-plus'/>}
/>
);
} else if (global.window.mm_config.EnableTeamCreation === 'true' || isSystemAdmin) {
teams.push(
<TeamButton
key='more_teams'
url='/create_team'
tip={
<FormattedMessage
id='navbar_dropdown.create'
defaultMessage='Create a New Team'
/>
}
contents={<i className='fa fa-plus'/>}
/>
);
}
return (
<div className='team-sidebar'>
<div className='team-wrapper'>
{teams}
</div>
</div>
);
}
}

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

@@ -50,6 +50,7 @@ export default class SidebarRight extends React.Component {
$('.app__body .inner-wrap').removeClass('move--right');
$('.app__body .inner-wrap').addClass('webrtc--show');
$('.app__body .sidebar--left').removeClass('move--right');
$('.multi-teams .team-sidebar').removeClass('move--right');
$('.app__body .webrtc').addClass('webrtc--show');
//$('.sidebar--right').prepend('<div class="sidebar__overlay"></div>');

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

@@ -1602,6 +1602,7 @@
"navbar_dropdown.help": "Help",
"navbar_dropdown.integrations": "Integrations",
"navbar_dropdown.inviteMember": "Invite New Member",
"navbar_dropdown.join": "Join Another Team",
"navbar_dropdown.leave": "Leave Team",
"navbar_dropdown.logout": "Logout",
"navbar_dropdown.manageMembers": "Manage Members",
@@ -1750,7 +1751,6 @@
"sidebar_right_menu.nativeApps": "Download Apps",
"sidebar_right_menu.recentMentions": "Recent Mentions",
"sidebar_right_menu.report": "Report a Problem",
"sidebar_right_menu.switch_team": "Team Selection",
"sidebar_right_menu.teamLink": "Get Team Invite Link",
"sidebar_right_menu.teamSettings": "Team Settings",
"sidebar_right_menu.viewMembers": "View Members",
@@ -1760,7 +1760,6 @@
"signup.ldap": "AD/LDAP Credentials",
"signup.office365": "Office 365",
"signup.title": "Create an account with:",
"signup_team.choose": "Your teams: ",
"signup_team.createTeam": "Or Create a Team",
"signup_team.disabled": "Team creation has been disabled. Please contact an administrator for access.",
"signup_team.join_open": "Teams you can join: ",
@@ -1853,6 +1852,7 @@
"team_settings_modal.generalTab": "General",
"team_settings_modal.importTab": "Import",
"team_settings_modal.title": "Team Settings",
"team_sidebar.join": "Other teams you can join.",
"textbox.bold": "**bold**",
"textbox.edit": "Edit message",
"textbox.help": "Help",
@@ -2170,6 +2170,7 @@
"web.footer.privacy": "Privacy",
"web.footer.terms": "Terms",
"web.header.back": "Back",
"web.header.logout": "Logout",
"web.root.signup_info": "All team communication in one place, searchable and accessible anywhere",
"webrtc.busy": "{username} is busy.",
"webrtc.call": "Call",

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

@@ -14,6 +14,7 @@ const ActionTypes = Constants.ActionTypes;
import * as AsyncClient from 'utils/async_client.jsx';
import Client from 'client/web_client.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import emojiRoute from 'routes/route_emoji.jsx';
import integrationsRoute from 'routes/route_integrations.jsx';
@@ -63,7 +64,7 @@ function preNeedsTeam(nextState, replace, callback) {
// First check to make sure you're in the current team
// for the current url.
const teamName = nextState.params.team;
var team = TeamStore.getByName(teamName);
const team = TeamStore.getByName(teamName);
if (!team) {
browserHistory.push('/');
@@ -75,9 +76,10 @@ function preNeedsTeam(nextState, replace, callback) {
TeamStore.saveMyTeam(team);
TeamStore.emitChange();
loadProfilesAndTeamMembersForDMSidebar();
AsyncClient.getMyTeamsUnread();
AsyncClient.getMyChannelMembers();
var d1 = $.Deferred(); //eslint-disable-line new-cap
const d1 = $.Deferred(); //eslint-disable-line new-cap
Client.getChannels(
(data) => {
@@ -101,6 +103,20 @@ function preNeedsTeam(nextState, replace, callback) {
});
}
function selectLastChannel(nextState, replace, callback) {
const team = TeamStore.getByName(nextState.params.team);
const channelId = PreferenceStore.get(team.id, 'channel');
const channel = ChannelStore.getChannelById(channelId);
let channelName = 'town-square';
if (channel) {
channelName = channel.name;
}
replace(`/${team.name}/channels/${channelName}`);
callback();
}
function onPermalinkEnter(nextState, replace, callback) {
const postId = nextState.params.postid;
GlobalActions.emitPostFocusEvent(
@@ -112,7 +128,7 @@ function onPermalinkEnter(nextState, replace, callback) {
export default {
path: ':team',
onEnter: preNeedsTeam,
indexRoute: {onEnter: (nextState, replace) => replace('/' + nextState.params.team + '/channels/town-square')},
indexRoute: {onEnter: selectLastChannel},
childRoutes: [
integrationsRoute,
emojiRoute,
@@ -126,10 +142,11 @@ export default {
onEnter: onChannelEnter,
getComponents: (location, callback) => {
Promise.all([
System.import('components/team_sidebar/team_sidebar_controller.jsx'),
System.import('components/sidebar.jsx'),
System.import('components/channel_view.jsx')
]).then(
(comarr) => callback(null, {sidebar: comarr[0].default, center: comarr[1].default})
(comarr) => callback(null, {team_sidebar: comarr[0].default, sidebar: comarr[1].default, center: comarr[2].default})
);
}
},
@@ -138,10 +155,11 @@ export default {
onEnter: onPermalinkEnter,
getComponents: (location, callback) => {
Promise.all([
System.import('components/team_sidebar/team_sidebar_controller.jsx'),
System.import('components/sidebar.jsx'),
System.import('components/permalink_view.jsx')
]).then(
(comarr) => callback(null, {sidebar: comarr[0].default, center: comarr[1].default})
(comarr) => callback(null, {team_sidebar: comarr[0].default, sidebar: comarr[1].default, center: comarr[2].default})
);
}
},
@@ -149,10 +167,11 @@ export default {
path: 'tutorial',
getComponents: (location, callback) => {
Promise.all([
System.import('components/team_sidebar/team_sidebar_controller.jsx'),
System.import('components/sidebar.jsx'),
System.import('components/tutorial/tutorial_view.jsx')
]).then(
(comarr) => callback(null, {sidebar: comarr[0].default, center: comarr[1].default})
(comarr) => callback(null, {team_sidebar: comarr[0].default, sidebar: comarr[1].default, center: comarr[2].default})
);
}
}

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

@@ -10,4 +10,5 @@
@import 'sidebar-left';
@import 'sidebar-menu';
@import 'sidebar-right';
@import 'team-sidebar';
@import 'webhooks';

90
webapp/sass/layout/_team-sidebar.scss Обычный файл
Просмотреть файл

@@ -0,0 +1,90 @@
.multi-teams {
.team-sidebar {
height: 100%;
left: 0;
position: fixed;
text-align: center;
width: 75px;
z-index: 5;
.team-wrapper {
background-color: rgba(0, 0, 0, 0.2);
height: 100%;
overflow: auto;
padding-top: 15px;
.team-container {
width: 100%;
text-align: center;
display: table;
margin-bottom: 10px;
position: relative;
.team-btn {
border: none;
height: 42px;
text-align: center;
font-weight: bold;
border-radius: 5px;
position: relative;
width: 42px;
background-color: rgba(0, 0, 0, 0.3);
display: table-cell;
vertical-align: middle;
.badge {
position: absolute;
top: -3px;
right: -6px;
font-size: 10px;
}
}
&.active {
.team-btn {
border: 1px solid #fff;
background-color: transparent;
}
}
&.active:before {
background: black;
content: '';
height: 100%;
left: 0;
position: absolute;
top: 0;
width: 5px;
}
&.unread:before {
background: black;
border-radius: 50%;
content: '';
height: 5px;
left: 0;
position: absolute;
top: 18px;
width: 5px;
}
a.team-disabled {
cursor: not-allowed;
opacity: 0.5;
}
}
.team-container a:hover {
text-decoration: none;
}
}
}
.sidebar--left {
left: 75px;
}
.app__content {
margin-left: 295px;
}
}

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

@@ -1163,6 +1163,33 @@
.post-comments {
padding: 9px 21px 10px 10px !important;
}
.multi-teams {
.team-sidebar {
width: 75px;
}
.app__content {
margin-left: 0;
}
.sidebar--left {
left: 0;
&.move--right {
left: 75px;
}
}
.team-sidebar {
display: none;
&.move--right {
display: block;
@include translate3d(0, 0, 0);
}
}
}
}
@media screen and (max-width: 640px) {
@@ -1493,6 +1520,20 @@
}
@media screen and (max-width: 320px) {
.multi-teams {
.team-sidebar {
width: 65px;
}
.sidebar--left {
width: 220px;
&.move--right {
left: 65px;
}
}
}
.post {
.post__header {
.col__name {

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

@@ -215,6 +215,10 @@ class ChannelStoreClass extends EventEmitter {
return this.channels;
}
getChannelById(id) {
return this.channels.filter((c) => c.id === id)[0];
}
storeMyChannelMember(channelMember) {
const members = Object.assign({}, this.getMyMembers());
members[channelMember.channel_id] = channelMember;
@@ -348,7 +352,6 @@ ChannelStore.dispatchToken = AppDispatcher.register((payload) => {
switch (action.type) {
case ActionTypes.CLICK_CHANNEL:
ChannelStore.setCurrentId(action.id);
ChannelStore.resetCounts(action.id);
ChannelStore.setPostMode(ChannelStore.POST_MODE_CHANNEL);
ChannelStore.emitChange();
break;

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

@@ -45,7 +45,7 @@ class NotificationStoreClass extends EventEmitter {
}
const teamId = msgProps.team_id;
const channel = ChannelStore.get(post.channel_id);
let channel = ChannelStore.get(post.channel_id);
const user = UserStore.getCurrentUser();
const member = ChannelStore.getMyMember(post.channel_id);
@@ -72,6 +72,9 @@ class NotificationStoreClass extends EventEmitter {
let title = Utils.localizeMessage('channel_loader.posted', 'Posted');
if (!channel) {
title = msgProps.channel_display_name;
channel = {
name: msgProps.channel_name
};
} else if (channel.type === Constants.DM_CHANNEL) {
title = Utils.localizeMessage('notification.dm', 'Direct Message');
} else {

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

@@ -140,9 +140,12 @@ class PreferenceStore extends EventEmitter {
}
this.emitChange();
break;
case ActionTypes.CLICK_CHANNEL:
this.setPreference(action.team_id, 'channel', action.id);
break;
}
}
}
global.PreferenceStore = new PreferenceStore();
export default global.PreferenceStore;
export default global.PreferenceStore;

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

@@ -10,6 +10,7 @@ const ActionTypes = Constants.ActionTypes;
const CHANGE_EVENT = 'change';
const STATS_EVENT = 'stats';
const UNREAD_EVENT = 'unread';
var Utils;
@@ -53,17 +54,31 @@ class TeamStoreClass extends EventEmitter {
this.removeListener(STATS_EVENT, callback);
}
emitUnreadChange() {
this.emit(UNREAD_EVENT);
}
addUnreadChangeListener(callback) {
this.on(UNREAD_EVENT, callback);
}
removeUnreadChangeListener(callback) {
this.removeListener(UNREAD_EVENT, callback);
}
get(id) {
var c = this.getAll();
return c[id];
}
getByName(name) {
var t = this.getAll();
const t = this.getAll();
for (var id in t) {
if (t[id].name === name) {
return t[id];
for (const id in t) {
if (t.hasOwnProperty(id)) {
if (t[id].name === name) {
return t[id];
}
}
}
@@ -158,6 +173,25 @@ class TeamStoreClass extends EventEmitter {
this.teams = teams;
}
updateTeam(team) {
const t = JSON.parse(team);
if (this.teams && this.teams[t.id]) {
this.teams[t.id] = t;
}
if (this.teamListings && this.teamListings[t.id]) {
if (t.allow_open_invite) {
this.teamListings[t.id] = t;
} else {
Reflect.deleteProperty(this.teamListings, t.id);
}
} else if (t.allow_open_invite) {
this.teamListings[t.id] = t;
}
this.emitChange();
}
saveMyTeam(team) {
this.saveTeam(team);
this.currentTeamId = team.id;
@@ -175,12 +209,29 @@ class TeamStoreClass extends EventEmitter {
this.my_team_members.push(member);
}
saveMyTeamMembersUnread(members) {
for (let i = 0; i < this.my_team_members.length; i++) {
const team = this.my_team_members[i];
const member = members.filter((m) => m.team_id === team.team_id)[0];
if (member) {
this.my_team_members[i] = Object.assign({},
team,
{
msg_count: member.msg_count,
mention_count: member.mention_count
});
}
}
}
removeMyTeamMember(teamId) {
for (let i = 0; i < this.my_team_members.length; i++) {
if (this.my_team_members[i].team_id === teamId) {
this.my_team_members.splice(i, 1);
}
}
this.emitChange();
}
getMyTeamMembers() {
@@ -248,6 +299,14 @@ class TeamStoreClass extends EventEmitter {
return false;
}
updateUnreadCount(teamId, totalMsgCount, channelMember) {
const member = this.my_team_members.filter((m) => m.team_id === teamId)[0];
if (member) {
member.msg_count -= (totalMsgCount - channelMember.msg_count);
member.mention_count -= channelMember.mention_count;
}
}
}
var TeamStore = new TeamStoreClass();
@@ -277,6 +336,10 @@ TeamStore.dispatchToken = AppDispatcher.register((payload) => {
TeamStore.saveMyTeamMembers(action.team_members);
TeamStore.emitChange();
break;
case ActionTypes.RECEIVED_MY_TEAMS_UNREAD:
TeamStore.saveMyTeamMembersUnread(action.team_members);
TeamStore.emitChange();
break;
case ActionTypes.RECEIVED_ALL_TEAM_LISTINGS:
TeamStore.saveTeamListings(action.teams);
TeamStore.emitChange();
@@ -292,6 +355,12 @@ TeamStore.dispatchToken = AppDispatcher.register((payload) => {
TeamStore.saveStats(action.team_id, action.stats);
TeamStore.emitStatsChange();
break;
case ActionTypes.CLICK_CHANNEL:
if (action.channelMember) {
TeamStore.updateUnreadCount(action.team_id, action.total_msg_count, action.channelMember);
TeamStore.emitUnreadChange();
}
break;
default:
}
});

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

@@ -130,6 +130,20 @@ describe('Client.Team', function() {
});
});
it('getMyTeamMembers', function(done) {
TestHelper.initBasic(() => {
TestHelper.basicClient().getMyTeamMembers(
function(data) {
assert.equal(data.length > 0, true);
done();
},
function(err) {
done(new Error(err.message));
}
);
});
});
it('getTeamMembers', function(done) {
TestHelper.initBasic(() => {
TestHelper.basicClient().getTeamMembers(

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

@@ -166,12 +166,21 @@ export function updateLastViewedAt(id, active) {
channelId,
isActive,
() => {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_PREFERENCE,
preference: {
category: 'last',
name: TeamStore.getCurrentId(),
value: channelId
}
});
callTracker[`updateLastViewed${channelId}`] = 0;
ErrorStore.clearLastError();
},
(err) => {
callTracker[`updateLastViewed${channelId}`] = 0;
var count = ErrorStore.getConnectionErrorCount();
const count = ErrorStore.getConnectionErrorCount();
ErrorStore.setConnectionErrorCount(count + 1);
dispatchError(err, 'updateLastViewedAt');
}
@@ -203,6 +212,14 @@ export function setLastViewedAt(lastViewedAt, id) {
channelId,
lastViewedAt,
() => {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_PREFERENCE,
preference: {
category: 'last',
name: TeamStore.getCurrentId(),
value: channelId
}
});
callTracker[`setLastViewedAt${channelId}${lastViewedAt}`] = 0;
ErrorStore.clearLastError();
},
@@ -847,6 +864,61 @@ export function getTeamMember(teamId, userId) {
);
}
export function getMyTeamMembers() {
const callName = 'getMyTeamMembers';
if (isCallInProgress(callName)) {
return;
}
callTracker[callName] = utils.getTimestamp();
Client.getMyTeamMembers(
(data) => {
callTracker[callName] = 0;
const members = {};
for (const member of data) {
members[member.team_id] = member;
}
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_MY_TEAM_MEMBERS_UNREAD,
team_members: members
});
},
(err) => {
callTracker[callName] = 0;
dispatchError(err, 'getMyTeamMembers');
}
);
}
export function getMyTeamsUnread(teamId) {
const members = TeamStore.getMyTeamMembers();
if (members.length > 1) {
const callName = 'getMyTeamsUnread';
if (isCallInProgress(callName)) {
return;
}
callTracker[callName] = utils.getTimestamp();
Client.getMyTeamsUnread(
teamId,
(data) => {
callTracker[callName] = 0;
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_MY_TEAMS_UNREAD,
team_members: data
});
},
(err) => {
callTracker[callName] = 0;
dispatchError(err, 'getMyTeamsUnread');
}
);
}
}
export function getTeamStats(teamId) {
const callName = `getTeamStats${teamId}`;
if (isCallInProgress(callName)) {

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

@@ -139,6 +139,7 @@ export const ActionTypes = keyMirror({
RECEIVED_ALL_TEAMS: null,
RECEIVED_ALL_TEAM_LISTINGS: null,
RECEIVED_MY_TEAM_MEMBERS: null,
RECEIVED_MY_TEAMS_UNREAD: null,
RECEIVED_MEMBERS_IN_TEAM: null,
RECEIVED_TEAM_STATS: null,
@@ -202,6 +203,7 @@ export const SocketEvents = {
DIRECT_ADDED: 'direct_added',
NEW_USER: 'new_user',
LEAVE_TEAM: 'leave_team',
UPDATE_TEAM: 'update_team',
USER_ADDED: 'user_added',
USER_REMOVED: 'user_removed',
USER_UPDATED: 'user_updated',

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

@@ -104,7 +104,9 @@ export function notifyMe(title, body, channel, teamId, duration, silent) {
var notification = new Notification(title, {body, tag: body, icon: icon50, requireInteraction: notificationDuration === 0, silent});
notification.onclick = () => {
window.focus();
if (channel) {
if (channel && channel.type === Constants.DM_CHANNEL) {
browserHistory.push(TeamStore.getCurrentTeamUrl() + '/channels/' + channel.name);
} else if (channel) {
browserHistory.push(TeamStore.getTeamUrl(teamId) + '/channels/' + channel.name);
} else if (teamId) {
browserHistory.push(TeamStore.getTeamUrl(teamId) + '/channels/town-square');
@@ -491,6 +493,8 @@ export function applyTheme(theme) {
changeCss('.sidebar--left .nav li.active a:before, .app__body .modal .settings-modal .nav-pills>li.active a:before', 'background:' + theme.sidebarTextActiveBorder);
changeCss('.sidebar--left .sidebar__divider:before', 'background:' + changeOpacity(theme.sidebarTextActiveBorder, 0.5));
changeCss('.sidebar--left .sidebar__divider', 'color:' + theme.sidebarTextActiveBorder);
changeCss('.multi-teams .team-sidebar .team-wrapper .team-container.active:before', 'background:' + theme.sidebarTextActiveBorder);
changeCss('.multi-teams .team-sidebar .team-wrapper .team-container.unread:before', 'background:' + theme.sidebarTextActiveBorder);
}
if (theme.sidebarTextActiveColor) {
@@ -501,13 +505,13 @@ export function applyTheme(theme) {
if (theme.sidebarHeaderBg) {
changeCss('.sidebar--left .team__header, .app__body .sidebar--menu .team__header, .app__body .post-list__timestamp > div', 'background:' + theme.sidebarHeaderBg);
changeCss('.app__body .modal .modal-header', 'background:' + theme.sidebarHeaderBg);
changeCss('.app__body #navbar .navbar-default', 'background:' + theme.sidebarHeaderBg);
changeCss('.app__body .multi-teams .team-sidebar, .app__body #navbar .navbar-default', 'background:' + theme.sidebarHeaderBg);
changeCss('@media(max-width: 768px){.app__body .search-bar__container', 'background:' + theme.sidebarHeaderBg);
changeCss('.app__body .attachment .attachment__container', 'border-left-color:' + theme.sidebarHeaderBg);
}
if (theme.sidebarHeaderTextColor) {
changeCss('.sidebar--left .team__header .header__info, .app__body .sidebar--menu .team__header .header__info, .app__body .post-list__timestamp > div', 'color:' + theme.sidebarHeaderTextColor);
changeCss('.multi-teams .team-sidebar .team-wrapper .team-container .team-btn, .sidebar--left .team__header .header__info, .app__body .sidebar--menu .team__header .header__info, .app__body .post-list__timestamp > div', 'color:' + theme.sidebarHeaderTextColor);
changeCss('.app__body .sidebar-header-dropdown__icon', 'fill:' + theme.sidebarHeaderTextColor);
changeCss('.sidebar--left .team__header .user__name, .app__body .sidebar--menu .team__header .user__name', 'color:' + changeOpacity(theme.sidebarHeaderTextColor, 0.8));
changeCss('.sidebar--left .team__header:hover .user__name, .app__body .sidebar--menu .team__header:hover .user__name', 'color:' + theme.sidebarHeaderTextColor);
@@ -534,11 +538,13 @@ export function applyTheme(theme) {
if (theme.mentionBj) {
changeCss('.sidebar--left .nav-pills__unread-indicator, .app__body .new-messages__button div', 'background:' + theme.mentionBj);
changeCss('.sidebar--left .badge', 'background:' + theme.mentionBj + '!important;');
changeCss('.multi-teams .team-sidebar .team-wrapper .team-container .team-btn .badge', 'background:' + theme.mentionBj + '!important;');
}
if (theme.mentionColor) {
changeCss('.sidebar--left .nav-pills__unread-indicator, .app__body .new-messages__button div', 'color:' + theme.mentionColor);
changeCss('.sidebar--left .badge', 'color:' + theme.mentionColor + '!important;');
changeCss('.multi-teams .team-sidebar .team-wrapper .team-container .team-btn .badge', 'color:' + theme.mentionColor + '!important;');
}
if (theme.centerChannelBg) {