Commit 413c6264 authored by Harrison Healey's avatar Harrison Healey

Merge branch 'master' into mark-as-unread

parents 4886198f 50fd53a0
......@@ -1627,8 +1627,7 @@
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
"dev": true
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
},
"atob": {
"version": "2.1.2",
......@@ -2213,7 +2212,6 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"requires": {
"delayed-stream": "~1.0.0"
}
......@@ -2495,8 +2493,7 @@
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
"dev": true
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
},
"detect-newline": {
"version": "2.1.0",
......@@ -3182,7 +3179,6 @@
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz",
"integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==",
"dev": true,
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.6",
......@@ -5427,14 +5423,12 @@
"mime-db": {
"version": "1.40.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
"integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==",
"dev": true
"integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA=="
},
"mime-types": {
"version": "2.1.24",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
"integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
"dev": true,
"requires": {
"mime-db": "1.40.0"
}
......
......@@ -25,6 +25,7 @@
"dev-mobile:watch": "ttsc --watch --outDir ${MOBILE_DIR:-../mattermost-mobile}/node_modules/mattermost-redux"
},
"dependencies": {
"form-data": "2.5.1",
"gfycat-sdk": "1.4.18",
"moment-timezone": "0.5.26",
"isomorphic-fetch": "2.2.1",
......@@ -72,7 +73,6 @@
"eslint-plugin-cypress": "2.7.0",
"eslint-plugin-header": "3.0.0",
"eslint-plugin-import": "2.18.2",
"form-data": "2.5.1",
"husky": "3.0.5",
"jest": "24.8.0",
"jest-junit": "6.4.0",
......
......@@ -1355,6 +1355,57 @@ describe('Actions.Channels', () => {
assert.ifError(myMembers[channel.id]);
});
it('getArchivedChannels', async () => {
const userClient = TestHelper.createClient4();
nock(Client4.getUsersRoute()).
post('').
query(true).
reply(201, TestHelper.fakeUserWithId());
const user = await TestHelper.basicClient4.createUser(
TestHelper.fakeUser(),
null,
null,
TestHelper.basicTeam.invite_id
);
nock(Client4.getUsersRoute()).
post('/login').
reply(200, user);
await userClient.login(user.email, 'password1');
nock(Client4.getChannelsRoute()).
post('').
reply(201, TestHelper.fakeChannelWithId(TestHelper.basicTeam.id));
const userChannel = await userClient.createChannel(
TestHelper.fakeChannel(TestHelper.basicTeam.id)
);
nock(Client4.getTeamsRoute()).
get(`/${TestHelper.basicTeam.id}/channels/deleted`).
query(true).
reply(200, [TestHelper.basicChannel, userChannel]);
await store.dispatch(Actions.getArchivedChannels(TestHelper.basicTeam.id, 0));
const moreRequest = store.getState().requests.channels.getChannels;
if (moreRequest.status === RequestStatus.FAILURE) {
throw new Error(JSON.stringify(moreRequest.error));
}
const {channels, channelsInTeam, myMembers} = store.getState().entities.channels;
const channel = channels[userChannel.id];
const team = channelsInTeam[userChannel.team_id];
assert.ok(channel);
assert.ok(team);
assert.ok(team.has(userChannel.id));
assert.ifError(myMembers[channel.id]);
});
it('getAllChannels', async () => {
const userClient = TestHelper.createClient4();
......@@ -1492,6 +1543,49 @@ describe('Actions.Channels', () => {
assert.ok(data.length === 2);
});
it('searchArchivedChannels', async () => {
const userClient = TestHelper.createClient4();
nock(Client4.getUsersRoute()).
post('').
query(true).
reply(201, TestHelper.fakeUserWithId());
const user = await TestHelper.basicClient4.createUser(
TestHelper.fakeUser(),
null,
null,
TestHelper.basicTeam.invite_id
);
nock(Client4.getUsersRoute()).
post('/login').
reply(200, user);
await userClient.login(user.email, 'password1');
nock(Client4.getChannelsRoute()).
post('').
reply(201, TestHelper.fakeChannelWithId(TestHelper.basicTeam.id));
const userChannel = await userClient.createChannel(
TestHelper.fakeChannel(TestHelper.basicTeam.id)
);
nock(Client4.getTeamsRoute()).
post(`/${TestHelper.basicTeam.id}/channels/search_archived`).
reply(200, [TestHelper.basicChannel, userChannel]);
const {data} = await store.dispatch(Actions.searchChannels(TestHelper.basicTeam.id, 'test', true));
const moreRequest = store.getState().requests.channels.getChannels;
if (moreRequest.status === RequestStatus.FAILURE) {
throw new Error(JSON.stringify(moreRequest.error));
}
assert.ok(data.length === 2);
});
it('getChannelMembers', async () => {
nock(Client4.getChannelsRoute()).
get(`/${TestHelper.basicChannel.id}/members`).
......
......@@ -844,6 +844,26 @@ export function getChannels(teamId: string, page = 0, perPage: number = General.
};
}
export function getArchivedChannels(teamId: string, page = 0, perPage: number = General.CHANNELS_CHUNK_SIZE): ActionFunc {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
let channels;
try {
channels = await Client4.getArchivedChannels(teamId, page, perPage);
} catch (error) {
forceLogoutIfNecessary(error, dispatch, getState);
return {error};
}
dispatch({
type: ChannelTypes.RECEIVED_CHANNELS,
teamId,
data: channels,
}, getState);
return {data: channels};
};
}
export function getAllChannelsWithCount(page = 0, perPage: number = General.CHANNELS_CHUNK_SIZE, notAssociatedToGroup = '', excludeDefaultChannels = false): ActionFunc {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
dispatch({type: ChannelTypes.GET_ALL_CHANNELS_REQUEST, data: null}, getState);
......@@ -970,13 +990,17 @@ export function autocompleteChannelsForSearch(teamId: string, term: string): Act
};
}
export function searchChannels(teamId: string, term: string): ActionFunc {
export function searchChannels(teamId: string, term: string, archived?: boolean): ActionFunc {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
dispatch({type: ChannelTypes.GET_CHANNELS_REQUEST, data: null}, getState);
let channels;
try {
channels = await Client4.searchChannels(teamId, term);
if (archived) {
channels = await Client4.searchArchivedChannels(teamId, term);
} else {
channels = await Client4.searchChannels(teamId, term);
}
} catch (error) {
forceLogoutIfNecessary(error, dispatch, getState);
dispatch(batchActions([
......
......@@ -513,6 +513,12 @@ describe('Actions.Websocket doReconnect', () => {
},
channels: {
currentChannelId,
channels: {
currentChannelId: {
id: currentChannelId,
name: 'channel',
},
},
},
users: {
currentUserId,
......@@ -569,7 +575,7 @@ describe('Actions.Websocket doReconnect', () => {
reply(200, []);
ChannelActions.fetchMyChannelsAndMembers = jest.fn().mockReturnValue({
type: MOCK_CHANNELS_REQUEST,
type: MOCK_CHANNELS_REQUEST, data: [], teamId: currentTeamId,
});
nock(Client4.getBaseRoute()).
get(`/users/me/teams/${currentTeamId}/channels`).
......@@ -594,7 +600,8 @@ describe('Actions.Websocket doReconnect', () => {
});
it('handle doReconnect', async () => {
const testStore = await mockStore(initialState);
const state = {...initialState};
const testStore = await mockStore(state);
const timestamp = 1000;
const expectedActions = [
......@@ -603,8 +610,7 @@ describe('Actions.Websocket doReconnect', () => {
{type: MOCK_MY_TEAM_UNREADS},
{type: MOCK_GET_MY_TEAMS},
{type: MOCK_GET_MY_TEAM_MEMBERS},
{type: MOCK_GET_POSTS},
{type: MOCK_CHANNELS_REQUEST},
{type: MOCK_CHANNELS_REQUEST, data: [], teamId: currentTeamId},
{type: MOCK_CHECK_FOR_MODIFIED_USERS},
{type: GeneralTypes.WEBSOCKET_SUCCESS, timestamp, data: null},
];
......@@ -614,6 +620,33 @@ describe('Actions.Websocket doReconnect', () => {
expect(testStore.getActions()).toEqual(expectedActions);
});
it('handle doReconnect after the current channel was archived or the user left it', async () => {
const state = {...initialState};
const testStore = await mockStore(state);
const timestamp = 1000;
const expectedActions = [
{type: MOCK_GET_PREFERENCES},
{type: MOCK_GET_STATUSES_BY_IDS},
{type: MOCK_MY_TEAM_UNREADS},
{type: MOCK_GET_MY_TEAMS},
{type: MOCK_GET_MY_TEAM_MEMBERS},
{type: MOCK_CHANNELS_REQUEST, data: [], teamId: currentTeamId},
{type: MOCK_CHECK_FOR_MODIFIED_USERS},
{type: GeneralTypes.WEBSOCKET_SUCCESS, timestamp, data: null},
];
const expectedMissingActions = [
{type: MOCK_GET_POSTS},
];
await testStore.dispatch(Actions.doReconnect(timestamp));
const actions = testStore.getActions();
expect(actions).toEqual(expect.arrayContaining(expectedActions));
expect(actions).not.toEqual(expect.arrayContaining(expectedMissingActions));
});
it('handle doReconnect after user left current team', async () => {
const state = {...initialState};
state.entities.teams.myMembers = {};
......@@ -625,7 +658,7 @@ describe('Actions.Websocket doReconnect', () => {
{type: MOCK_MY_TEAM_UNREADS},
{type: MOCK_GET_MY_TEAMS},
{type: MOCK_GET_MY_TEAM_MEMBERS},
{type: TeamTypes.LEAVE_TEAM, data: initialState.entities.teams.teams[currentTeamId]},
{type: TeamTypes.LEAVE_TEAM, data: state.entities.teams.teams[currentTeamId]},
{type: MOCK_CHECK_FOR_MODIFIED_USERS},
{type: GeneralTypes.WEBSOCKET_SUCCESS, timestamp, data: null},
];
......
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Client4} from 'client';
import websocketClient from '../client/websocket_client';
......@@ -22,7 +23,7 @@ import {getTeam, getMyTeamUnreads, getMyTeams, getMyTeamMembers} from './teams';
import {getPost, getPosts, getProfilesAndStatusesForPosts, getCustomEmojiForReaction, getUnreadPostData, handleNewPost, postDeleted, receivedPost} from './posts';
import {fetchMyChannelsAndMembers, getChannelAndMyMember, getChannelStats, markChannelAsRead} from './channels';
import {checkForModifiedUsers, getMe, getProfilesByIds, getStatusesByIds, loadProfilesForDirect} from './users';
import {ChannelMembership} from 'types/channels';
import {Channel, ChannelMembership} from 'types/channels';
import {Dictionary} from 'types/utilities';
import {PreferenceType} from 'types/preferences';
let doDispatch: DispatchFunc;
......@@ -135,16 +136,18 @@ export function doReconnect(now: number) {
await dispatch(getMyTeamMembers());
const currentTeamMembership = getCurrentTeamMembership(getState());
if (currentTeamMembership) {
dispatch(getPosts(currentChannelId));
const fethcResult = await dispatch(fetchMyChannelsAndMembers(currentTeamId));
const data = (fethcResult as any).data || null;
dispatch(loadProfilesForDirect());
if (data && data.members) {
if (data && data.channels && data.members) {
const channelStillExists = data.channels.find((c: Channel) => c.id === currentChannelId);
const stillMemberOfCurrentChannel = data.members.find((m: ChannelMembership) => m.channel_id === currentChannelId);
if (!stillMemberOfCurrentChannel) {
if (!stillMemberOfCurrentChannel || !channelStillExists) {
EventEmitter.emit(General.SWITCH_TO_DEFAULT_CHANNEL, currentTeamId);
} else {
dispatch(getPosts(currentChannelId));
}
}
} else {
......
......@@ -1403,6 +1403,12 @@ export default class Client4 {
{method: 'get'}
);
};
getArchivedChannels = async (teamId: string, page = 0, perPage = PER_PAGE_DEFAULT) => {
return this.doFetch(
`${this.getTeamRoute(teamId)}/channels/deleted${buildQueryString({page, per_page: perPage})}`,
{method: 'get'}
);
};
getMyChannels = async (teamId: string) => {
return this.doFetch(
......@@ -1515,6 +1521,13 @@ export default class Client4 {
);
};
searchArchivedChannels = async (teamId: string, term: string) => {
return this.doFetch(
`${this.getTeamRoute(teamId)}/channels/search_archived`,
{method: 'post', body: JSON.stringify({term})}
);
};
searchAllChannels = async (term: string, notAssociatedToGroup = '', excludeDefaultChannels = false) => {
const body = {
term,
......
......@@ -5,14 +5,15 @@ import {Dictionary} from 'types/utilities';
const Files: Dictionary<string[]> = {
AUDIO_TYPES: ['mp3', 'wav', 'wma', 'm4a', 'flac', 'aac', 'ogg'],
CODE_TYPES: ['as', 'applescript', 'osascript', 'scpt', 'bash', 'sh', 'zsh', 'clj', 'boot', 'cl2', 'cljc', 'cljs', 'cljs.hl', 'cljscm', 'cljx', 'hic', 'coffee', '_coffee', 'cake', 'cjsx', 'cson', 'iced', 'cpp', 'c', 'cc', 'h', 'c++', 'h++', 'hpp', 'cs', 'csharp', 'css', 'd', 'di', 'dart', 'delphi', 'dpr', 'dfm', 'pas', 'pascal', 'freepascal', 'lazarus', 'lpr', 'lfm', 'diff', 'django', 'jinja', 'dockerfile', 'docker', 'erl', 'f90', 'f95', 'fsharp', 'fs', 'gcode', 'nc', 'go', 'groovy', 'handlebars', 'hbs', 'html.hbs', 'html.handlebars', 'hs', 'hx', 'java', 'jsp', 'js', 'jsx', 'json', 'jl', 'kt', 'ktm', 'kts', 'less', 'lisp', 'lua', 'mk', 'mak', 'md', 'mkdown', 'mkd', 'matlab', 'm', 'mm', 'objc', 'obj-c', 'ml', 'perl', 'pl', 'php', 'php3', 'php4', 'php5', 'php6', 'ps', 'ps1', 'pp', 'py', 'gyp', 'r', 'ruby', 'rb', 'gemspec', 'podspec', 'thor', 'irb', 'rs', 'scala', 'scm', 'sld', 'scss', 'st', 'sql', 'swift', 'tex', 'txt', 'vbnet', 'vb', 'bas', 'vbs', 'v', 'veo', 'xml', 'html', 'xhtml', 'rss', 'atom', 'xsl', 'plist', 'yaml'],
CODE_TYPES: ['as', 'applescript', 'osascript', 'scpt', 'bash', 'sh', 'zsh', 'clj', 'boot', 'cl2', 'cljc', 'cljs', 'cljs.hl', 'cljscm', 'cljx', 'hic', 'coffee', '_coffee', 'cake', 'cjsx', 'cson', 'iced', 'cpp', 'c', 'cc', 'h', 'c++', 'h++', 'hpp', 'cs', 'csharp', 'css', 'd', 'di', 'dart', 'delphi', 'dpr', 'dfm', 'pas', 'pascal', 'freepascal', 'lazarus', 'lpr', 'lfm', 'diff', 'django', 'jinja', 'dockerfile', 'docker', 'erl', 'f90', 'f95', 'fsharp', 'fs', 'gcode', 'nc', 'go', 'groovy', 'handlebars', 'hbs', 'html.hbs', 'html.handlebars', 'hs', 'hx', 'java', 'jsp', 'js', 'jsx', 'json', 'jl', 'kt', 'ktm', 'kts', 'less', 'lisp', 'lua', 'mk', 'mak', 'md', 'mkdown', 'mkd', 'matlab', 'm', 'mm', 'objc', 'obj-c', 'ml', 'perl', 'pl', 'php', 'php3', 'php4', 'php5', 'php6', 'ps', 'ps1', 'pp', 'py', 'gyp', 'r', 'ruby', 'rb', 'gemspec', 'podspec', 'thor', 'irb', 'rs', 'scala', 'scm', 'sld', 'scss', 'st', 'sql', 'swift', 'tex', 'vbnet', 'vb', 'bas', 'vbs', 'v', 'veo', 'xml', 'html', 'xhtml', 'rss', 'atom', 'xsl', 'plist', 'yaml'],
IMAGE_TYPES: ['jpg', 'gif', 'bmp', 'png', 'jpeg', 'tiff', 'tif'],
PATCH_TYPES: ['patch'],
PDF_TYPES: ['pdf'],
PRESENTATION_TYPES: ['ppt', 'pptx'],
SPREADSHEET_TYPES: ['xlsx', 'csv'],
TEXT_TYPES: ['txt', 'rtf'],
VIDEO_TYPES: ['mp4', 'avi', 'webm', 'mkv', 'wmv', 'mpg', 'mov', 'flv'],
WORD_TYPES: ['doc', 'docx'],
};
export default Files;
\ No newline at end of file
export default Files;
......@@ -10,6 +10,18 @@ import {Team} from 'types/teams';
function channelListToSet(state: any, action: GenericAction) {
const nextState = {...state};
const teamChannelIds = nextState[action.teamId];
// Remove existing channels that are no longer
if (teamChannelIds && teamChannelIds.size) {
teamChannelIds.forEach((id: string) => {
if (!action.data.find((c: any) => c.id === id)) {
teamChannelIds.delete(id);
}
});
nextState[action.teamId] = teamChannelIds;
}
action.data.forEach((channel: Channel) => {
const nextSet = new Set(nextState[channel.team_id]);
nextSet.add(channel.id);
......@@ -54,6 +66,18 @@ function channels(state: IDMappedObjects<Channel> = {}, action: GenericAction) {
case ChannelTypes.RECEIVED_ALL_CHANNELS:
case SchemeTypes.RECEIVED_SCHEME_CHANNELS: {
const nextState = {...state};
const currentChannels = Object.values(nextState);
// Remove existing channels that are no longer
currentChannels.forEach((channel) => {
if (channel.team_id === action.teamId) {
const id: string = channel.id;
if (!action.data.find((c: any) => c.id === id)) {
Reflect.deleteProperty(nextState, id);
}
}
});
for (const channel of action.data) {
if (state[channel.id] && channel.type === General.DM_CHANNEL) {
channel.display_name = channel.display_name || state[channel.id].display_name;
......
......@@ -127,13 +127,13 @@ function storeFilesIdsForPost(state: Dictionary<string[]>, post: Post) {
};
}
function filePublicLink(state: string | null = null, action: GenericAction) {
function filePublicLink(state: {link: string} = {link: ''}, action: GenericAction) {
switch (action.type) {
case FileTypes.RECEIVED_FILE_PUBLIC_LINK: {
return action.data;
}
case UserTypes.LOGOUT_SUCCESS:
return '';
return {link: ''};
default:
return state;
......
......@@ -40,6 +40,7 @@ export function getFileType(file: FileInfo): string {
'video',
'audio',
'spreadsheet',
'text',
'word',
'presentation',
'patch',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment