Remove insights (#23952)

* removed server side

* Updated store layer

* unused import

* Updated autogenerated code template

* Updated tests

* lint fix

* unused translations

* webapp side

* Updated i18n

* lint fix:

* type fix

* Updated snapshots

* Removed insights from API specs

* updated e2e

* Updated e2e tests

* Updated e2e tests

* Removed insights tests

* Removed Insights as possible channel to load in sidebar from test

* Removed more insights tests

* More e2e fixed

* More cleanup

* Lint

* More cleanup in client4 and boards api

* More cleanup

* Fixes

* lint fix

---------

Co-authored-by: maria.nunez <maria.nunez@mattermost.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
Harshil Sharma 2023-07-25 12:34:38 +05:30 committed by GitHub
parent e37459cd00
commit 26617fcbdc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
192 changed files with 47 additions and 19885 deletions

View File

@ -11,7 +11,6 @@ build-v4: node_modules playbooks
@echo Building mattermost openapi yaml for v4
@if [ -r $(PLAYBOOKS_SRC)/merged-tags.yaml ]; then cat $(PLAYBOOKS_SRC)/merged-tags.yaml > $(V4_YAML); else cat $(V4_SRC)/introduction.yaml > $(V4_YAML); fi
@cat $(V4_SRC)/insights.yaml >> $(V4_YAML)
@cat $(V4_SRC)/users.yaml >> $(V4_YAML)
@cat $(V4_SRC)/status.yaml >> $(V4_YAML)
@cat $(V4_SRC)/teams.yaml >> $(V4_YAML)

View File

@ -797,16 +797,6 @@ components:
description: The time in milliseconds this reaction was made
type: integer
format: int64
TopReaction:
type: object
properties:
emoji_name:
description: The name of the emoji used for this reaction.
type: string
count:
description: The number of the times this emoji has been used.
type: integer
format: int64
NewTeamMember:
type: object
properties:
@ -827,17 +817,6 @@ components:
create_at:
description: The creation timestamp of the team member record.
type: integer
TopReactionList:
type: object
properties:
has_next:
description: Indicates if there is another page of reactions that can be fetched.
type: boolean
items:
description: List of reactions.
type: array
items:
$ref: "#/components/schemas/TopReaction"
NewTeamMembersList:
type: object
properties:
@ -852,109 +831,6 @@ components:
total_count:
description: The total count of new team members for the given time range.
type: integer
TopChannel:
type: object
properties:
id:
type: string
type:
type: string
display_name:
type: string
name:
type: string
team_id:
type: string
message_count:
description: The number of messages posted in the channel by users over the given time period (not including messages posted by bots).
type: string
TopChannelList:
type: object
properties:
has_next:
description: Indicates if there is another page of channels that can be fetched.
type: boolean
items:
description: List of channels.
type: array
items:
$ref: "#/components/schemas/TopChannel"
InsightUserInformation:
type: object
properties:
id:
type: string
first_name:
type: string
last_name:
type: string
nickname:
type: string
username:
type: string
last_picture_update:
type: string
create_at:
type: integer
format: int64
TopThread:
type: object
properties:
post:
$ref: "#/components/schemas/Post"
channel_id:
type: string
channel_display_name:
type: string
channel_name:
type: string
Participants:
type: array
items:
type: string
user_information:
description: User who created the post
$ref: "#/components/schemas/InsightUserInformation"
TopThreadList:
type: object
properties:
has_next:
description: Indicates if there is another page of top threads that can be fetched.
type: boolean
items:
description: List of top threads.
type: array
items:
$ref: "#/components/schemas/TopThread"
TopDMInsightUserInformation:
allOf:
- $ref: "#/components/schemas/InsightUserInformation"
- type: object
properties:
position:
type: string
TopDM:
type: object
properties:
post_count:
type: integer
format: int64
outgoing_message_count:
type: integer
format: int64
second_participant:
$ref: "#/components/schemas/TopDMInsightUserInformation"
TopDMList:
type: object
properties:
has_next:
description: Indicates if there is another page of top DMs that can be fetched.
type: boolean
items:
description: List of top DMs.
type: array
items:
$ref: "#/components/schemas/TopDM"
Emoji:
type: object
properties:

View File

@ -1,439 +0,0 @@
/api/v4/teams/{team_id}/top/reactions:
get:
tags:
- insights
summary: Get a list of the top reactions for a team.
description: |
Get a list of the top reactions across all public and private channels (the user is a member of) for a given team.
##### Permissions
Must have `view_team` permission for the team.
operationId: GetTopReactionsForTeam
parameters:
- name: team_id
in: path
description: Team GUID
required: true
schema:
type: string
- name: time_range
in: query
description: >
Time range can be "today", "7_day", or "28_day".
- `today`: reactions posted on the current day.
- `7_day`: reactions posted in the last 7 days.
- `28_day`: reactions posted in the last 28 days.
required: true
schema:
type: string
- name: page
in: query
description: The page to select.
schema:
type: integer
default: 0
- name: per_page
in: query
description: The number of items per page, up to a maximum of 200.
schema:
type: integer
default: 60
responses:
"200":
description: Top reactions retrieved successfully.
content:
application/json:
schema:
type: object
$ref: "#/components/schemas/TopReactionList"
"400":
$ref: "#/components/responses/BadRequest"
"403":
$ref: "#/components/responses/Forbidden"
/api/v4/users/me/top/reactions:
get:
tags:
- insights
summary: Get a list of the top reactions for a user.
description: |
Get a list of the top reactions across all public and private channels (the user is a member of) for a given user.
If no `team_id` is provided, this will also include reactions posted by the given user in direct and group messages.
##### Permissions
Must be logged in as the user.
operationId: GetTopReactionsForUser
parameters:
- name: time_range
in: query
description: >
Time range can be "today", "7_day", or "28_day".
- `today`: reactions posted on the current day.
- `7_day`: reactions posted in the last 7 days.
- `28_day`: reactions posted in the last 28 days.
required: true
schema:
type: string
- name: page
in: query
description: The page to select.
schema:
type: integer
default: 0
- name: per_page
in: query
description: The number of items per page, up to a maximum of 200.
schema:
type: integer
default: 60
- name: team_id
in: query
description: >
Team ID will scope the response to a given team and exclude direct and group messages.
##### Permissions
Must have `view_team` permission for the team.
schema:
type: string
responses:
"200":
description: Top reactions retrieved successfully.
content:
application/json:
schema:
type: object
$ref: "#/components/schemas/TopReactionList"
"400":
$ref: "#/components/responses/BadRequest"
"403":
$ref: "#/components/responses/Forbidden"
/api/v4/teams/{team_id}/top/channels:
get:
tags:
- insights
summary: Get a list of the top channels for a team.
description: |
Get a list of the top public and private channels (the user is a member of) for a given team.
##### Permissions
Must have `view_team` permission for the team.
operationId: GetTopChannelsForTeam
parameters:
- name: team_id
in: path
description: Team GUID
required: true
schema:
type: string
- name: time_range
in: query
description: >
Time range can be "today", "7_day", or "28_day".
- `today`: channels with posts on the current day.
- `7_day`: channels with posts in the last 7 days.
- `28_day`: channels with posts in the last 28 days.
required: true
schema:
type: string
- name: page
in: query
description: The page to select.
schema:
type: integer
default: 0
- name: per_page
in: query
description: The number of items per page, up to a maximum of 200.
schema:
type: integer
default: 60
responses:
"200":
description: Top channels retrieved successfully.
content:
application/json:
schema:
type: object
$ref: "#/components/schemas/TopChannelList"
"400":
$ref: "#/components/responses/BadRequest"
"403":
$ref: "#/components/responses/Forbidden"
/api/v4/users/me/top/channels:
get:
tags:
- insights
summary: Get a list of the top channels for a user.
description: |
Get a list of the top public and private channels (the user is a member of) for a given user.
##### Permissions
Must be logged in as the user.
operationId: GetTopChannelsForUser
parameters:
- name: time_range
in: query
description: >
Time range can be "today", "7_day", or "28_day".
- `today`: channels with posts on the current day.
- `7_day`: channels with posts in the last 7 days.
- `28_day`: channels with posts in the last 28 days.
required: true
schema:
type: string
- name: page
in: query
description: The page to select.
schema:
type: integer
default: 0
- name: per_page
in: query
description: The number of items per page, up to a maximum of 200.
schema:
type: integer
default: 60
- name: team_id
in: query
description: >
Team ID will scope the response to a given team.
##### Permissions
Must have `view_team` permission for the team.
schema:
type: string
responses:
"200":
description: Top channels retrieved successfully.
content:
application/json:
schema:
type: object
$ref: "#/components/schemas/TopChannelList"
"400":
$ref: "#/components/responses/BadRequest"
"403":
$ref: "#/components/responses/Forbidden"
/api/v4/teams/{team_id}/top/team_members:
get:
tags:
- insights
summary: Get a list of new team members.
description: |
Get a list of all of the new team members that have joined the given team during the given time period.
##### Permissions
Must have `view_team` permission for the team.
operationId: GetNewTeamMembers
parameters:
- name: team_id
in: path
description: Team GUID
required: true
schema:
type: string
- name: time_range
in: query
description: >
Time range can be "today", "7_day", or "28_day".
- `today`: team members who joined during the current day.
- `7_day`: team members who joined in the last 7 days.
- `28_day`: team members who joined in the last 28 days.
required: true
schema:
type: string
- name: page
in: query
description: The page to select.
schema:
type: integer
default: 0
- name: per_page
in: query
description: The number of items per page.
schema:
type: integer
default: 60
responses:
"200":
description: New team members retrieved successfully.
content:
application/json:
schema:
type: object
$ref: "#/components/schemas/NewTeamMembersList"
"400":
$ref: "#/components/responses/BadRequest"
"403":
$ref: "#/components/responses/Forbidden"
/api/v4/teams/{team_id}/top/threads:
get:
tags:
- insights
summary: Get a list of the top threads for a team.
description: |
Get a list of the top threads from public and private channels (the user is a member of) for a given team.
##### Permissions
Must have `view_team` permission for the team.
operationId: GetTopThreadsForTeam
parameters:
- name: team_id
in: path
description: Team GUID
required: true
schema:
type: string
- name: time_range
in: query
description: >
Time range can be "today", "7_day", or "28_day".
- `today`: threads with activity on the current day.
- `7_day`: threads with activity in the last 7 days.
- `28_day`: threads with activity in the last 28 days.
required: true
schema:
type: string
- name: page
in: query
description: The page to select.
schema:
type: integer
default: 0
- name: per_page
in: query
description: The number of items per page, up to a maximum of 200.
schema:
type: integer
default: 60
responses:
"200":
description: Top threads retrieved successfully.
content:
application/json:
schema:
type: object
$ref: "#/components/schemas/TopThreadList"
"400":
$ref: "#/components/responses/BadRequest"
"403":
$ref: "#/components/responses/Forbidden"
/api/v4/users/me/top/threads:
get:
tags:
- insights
summary: Get a list of the top threads for a user.
description: |
Get a list of the top threads from public and private channels (the user is a member of and participating in the thread) for a given user.
##### Permissions
Must be logged in as the user.
operationId: GetTopThreadsForUser
parameters:
- name: time_range
in: query
description: >
Time range can be "today", "7_day", or "28_day".
- `today`: threads with activity on the current day.
- `7_day`: threads with activity in the last 7 days.
- `28_day`: threads with activity in the last 28 days.
required: true
schema:
type: string
- name: page
in: query
description: The page to select.
schema:
type: integer
default: 0
- name: per_page
in: query
description: The number of items per page, up to a maximum of 200.
schema:
type: integer
default: 60
- name: team_id
in: query
description: >
Team ID will scope the response to a given team.
##### Permissions
Must have `view_team` permission for the team.
schema:
type: string
responses:
"200":
description: Top threads retrieved successfully.
content:
application/json:
schema:
type: object
$ref: "#/components/schemas/TopThreadList"
"400":
$ref: "#/components/responses/BadRequest"
"403":
$ref: "#/components/responses/Forbidden"
/api/v4/users/me/top/dms:
get:
tags:
- insights
summary: Get a list of the top dms for a user.
description: |
Get a list of the top dms for a given user.
##### Permissions
Must be logged in as the user.
operationId: GetTopDMsForUser
parameters:
- name: time_range
in: query
description: >
Time range can be "today", "7_day", or "28_day".
- `today`: threads with activity on the current day.
- `7_day`: threads with activity in the last 7 days.
- `28_day`: threads with activity in the last 28 days.
required: true
schema:
type: string
- name: page
in: query
description: The page to select.
schema:
type: integer
default: 0
- name: per_page
in: query
description: The number of items per page, up to a maximum of 200.
schema:
type: integer
default: 60
responses:
"200":
description: Top dms retrieved successfully.
content:
application/json:
schema:
type: object
$ref: "#/components/schemas/TopDMList"
"400":
$ref: "#/components/responses/BadRequest"
"403":
$ref: "#/components/responses/Forbidden"

View File

@ -579,8 +579,6 @@ tags:
description: Endpoints for interactive actions for use by integrations.
- name: shared channels
description: Endpoints for getting information about shared channels.
- name: insights
description: Endpoints for getting insights into teams and users.
- name: terms of service
description: Endpoints for getting and updating custom terms of service.
- name: imports
@ -635,7 +633,6 @@ x-tagGroups:
- schemes
- integration_actions
- shared channels
- insights
- terms of service
- imports
- permissions

View File

@ -27,7 +27,6 @@ services:
MM_SERVICESETTINGS_ENABLEONBOARDINGFLOW: "false"
MM_FEATUREFLAGS_ONBOARDINGTOURTIPS: "false"
MM_SERVICEENVIRONMENT: "test"
MM_FEATUREFLAGS_INSIGHTSENABLED: "true"
volumes:
- "server-config:/mattermost/config"
ports:

View File

@ -76,9 +76,6 @@ describe('Verify Accessibility Support in Channel Sidebar Navigation', () => {
cy.findByRole('button', {name: 'Find Channels'}).should('be.focused');
cy.focused().tab();
// * Verify if Insights has focus
cy.focused().should('have.text', 'Insights').tab();
// * Verify if focus changes to different channels in Unread section
cy.get('.SidebarChannel.unread').each((el) => {
cy.wrap(el).find('.unread-title').should('be.focused');
@ -99,7 +96,7 @@ describe('Verify Accessibility Support in Channel Sidebar Navigation', () => {
it('MM-T1473 Verify Tab Support in Unreads section', () => {
// # Press tab from the Main Menu button
cy.uiGetLHSAddChannelButton().focus().tab().tab().tab();
cy.uiGetLHSAddChannelButton().focus().tab().tab();
// * Verify if focus changes to different channels in Unread section
cy.get('.SidebarChannel.unread').each((el) => {
@ -114,7 +111,7 @@ describe('Verify Accessibility Support in Channel Sidebar Navigation', () => {
markAsFavorite(testChannel.name);
// # Press tab from the add channel button down to all unread channels
cy.uiGetLHSAddChannelButton().focus().tab().tab().tab();
cy.uiGetLHSAddChannelButton().focus().tab().tab();
cy.get('.SidebarChannel.unread').each(() => {
cy.focused().tab().tab();
});

View File

@ -1,48 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// ***************************************************************
// - [#] indicates a test step (e.g. # Go to a page)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element ID when selecting an element. Create one if none.
// ***************************************************************
// Stage: @prod
describe('Insights', () => {
let teamA;
before(() => {
cy.shouldHaveFeatureFlag('InsightsEnabled', true);
cy.apiInitSetup().then(({team}) => {
teamA = team;
});
});
it('Check all the cards exist', () => {
cy.apiAdminLogin();
// # Go to the Insights view
cy.visit(`/${teamA.name}/activity-and-insights`);
// * Check top channels exists
cy.get('.top-channels-card').should('exist');
// * Check top threads exists
cy.get('.top-threads-card').should('exist');
// * Check top boards exists because product mode is enabled
cy.get('.top-boards-card').should('exist');
// * Check top reactions exists
cy.get('.top-reactions-card').should('exist');
// * Check top dms exists
cy.get('.top-dms-card').should('exist');
// * Check least active channels exists
cy.get('.least-active-channels-card').should('exist');
// * Check top playbooks exists because product mode is enabled
cy.get('.top-playbooks-card').should('exist');
});
});

View File

@ -1,79 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// ***************************************************************
// - [#] indicates a test step (e.g. # Go to a page)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element ID when selecting an element. Create one if none.
// ***************************************************************
// Stage: @prod
import * as TIMEOUTS from '../../../fixtures/timeouts';
describe('Insights as last viewed channel', () => {
let userA; // Member of team A and B
let teamA;
let teamB;
let offTopicUrlA;
let testChannel;
before(() => {
cy.shouldHaveFeatureFlag('InsightsEnabled', true);
cy.apiInitSetup().then(({team, user, offTopicUrl: url}) => {
userA = user;
teamA = team;
offTopicUrlA = url;
cy.apiCreateUser().then(() => {
return cy.apiCreateTeam('team', 'Team');
}).then(({team: otherTeam}) => {
teamB = otherTeam;
return cy.apiAddUserToTeam(teamB.id, userA.id);
}).then(() => {
return cy.apiCreateChannel(teamA.id, 'test', 'Test');
}).then(({channel}) => {
testChannel = channel;
return cy.apiAddUserToChannel(testChannel.id, userA.id);
});
});
});
beforeEach(() => {
// # Log in to Team A with an account that has joined multiple teams.
cy.apiLogin(userA);
// # On an account on two teams, view Team A
cy.visit(offTopicUrlA);
});
it('MM-T4902_1 Should go to insights view when switching a team if that was the last view on that team', () => {
// # Go to the Insights view on Team A
cy.uiGetSidebarInsightsButton().click().wait(TIMEOUTS.FIVE_SEC);
// # Switch to Team B
cy.get(`#${teamB.name}TeamButton`, {timeout: TIMEOUTS.ONE_MIN}).should('be.visible').click();
// * Verify team display name changes correctly.
cy.uiGetLHSHeader().findByText(teamB.display_name);
// # Switch back to Team A
cy.get(`#${teamA.name}TeamButton`, {timeout: TIMEOUTS.ONE_MIN}).should('be.visible').click();
// * Verify url is set up for insights view
cy.url().should('include', `${teamA.name}/activity-and-insights`);
});
it('MM-T4902_2 Should go to insights view when insights view is the penultimate view and leave the current channel', () => {
// # Go to the Insights view on Team A
cy.uiGetSidebarInsightsButton().click().wait(TIMEOUTS.FIVE_SEC);
// # Switch to Test Channel
cy.uiClickSidebarItem(testChannel.name);
// # Leave the current channel
cy.uiLeaveChannel();
// * Verify url is set up for insights view when insights view is the penultimate view
cy.url().should('include', `${teamA.name}/activity-and-insights`);
});
});

View File

@ -20,8 +20,6 @@ describe('Keyboard Shortcuts', () => {
let testTeam;
let publicChannel;
let privateChannel;
const insights = 'Insights';
const insightsSidebar = {name: insights};
before(() => {
cy.apiGetMe().then(({user: adminUser}) => {
@ -63,11 +61,8 @@ describe('Keyboard Shortcuts', () => {
verifyChannelSwitch(testTeam.name, offTopic, privateChannel, '{uparrow}');
verifyChannelSwitch(testTeam.name, publicChannel, offTopic, '{uparrow}');
// * Should switch to Insights
verifyChannelSwitch(testTeam.name, insightsSidebar, publicChannel, '{uparrow}');
// * Should switch to bottom of channel list when current channel is at the very top
verifyChannelSwitch(testTeam.name, dmWithSysadmin, insightsSidebar, '{uparrow}');
verifyChannelSwitch(testTeam.name, dmWithSysadmin, publicChannel, '{uparrow}');
});
it('MM-T1230 Alt/Option + Down', () => {
@ -81,10 +76,6 @@ describe('Keyboard Shortcuts', () => {
verifyChannelSwitch(testTeam.name, privateChannel, offTopic, '{downarrow}');
verifyChannelSwitch(testTeam.name, townSquare, privateChannel, '{downarrow}');
verifyChannelSwitch(testTeam.name, dmWithSysadmin, townSquare, '{downarrow}');
// * Should switch to top (Insights) when current channel is at the very bottom
verifyChannelSwitch(testTeam.name, insightsSidebar, dmWithSysadmin, '{downarrow}');
verifyChannelSwitch(testTeam.name, publicChannel, insightsSidebar, '{downarrow}');
});
function verifyChannelSwitch(teamName, toChannel, fromChannel, arrowKey) {
@ -94,8 +85,6 @@ describe('Keyboard Shortcuts', () => {
// * Verify that it redirects into expected URL
if (toChannel.type === 'D') {
cy.url().should('include', `/${teamName}/messages/@${toChannel.name}`);
} else if (toChannel.name === insights) {
cy.url().should('include', `/${teamName}/activity-and-insights`);
} else {
cy.url().should('include', `/${teamName}/channels/${toChannel.name}`);
}
@ -109,21 +98,16 @@ describe('Keyboard Shortcuts', () => {
});
function verifyClass(channel, assertion) {
if (channel.type) {
let label;
if (channel.type === 'O') {
label = channel.display_name.toLowerCase() + ' public channel';
} else if (channel.type === 'P') {
label = channel.display_name.toLowerCase() + ' private channel';
} else if (channel.type === 'D') {
label = channel.display_name.toLowerCase();
}
cy.findByLabelText(label).parent().should(assertion, 'active');
} else {
// For Insights
cy.findByText(channel.name).parent().parent().parent().should(assertion, 'active');
let label;
if (channel.type === 'O') {
label = channel.display_name.toLowerCase() + ' public channel';
} else if (channel.type === 'P') {
label = channel.display_name.toLowerCase() + ' private channel';
} else if (channel.type === 'D') {
label = channel.display_name.toLowerCase();
}
cy.findByLabelText(label).parent().should(assertion, 'active');
}
}
});

View File

@ -25,10 +25,6 @@ describe('Forward Message', () => {
const commentMessage = 'Comment for the forwarded message';
before(() => {
// # Testing Forwarding from Insights view requires a license
cy.apiRequireLicense();
cy.shouldHaveFeatureFlag('InsightsEnabled', true);
cy.apiUpdateConfig({
ServiceSettings: {
ThreadAutoFollow: true,

View File

@ -27,10 +27,6 @@ describe('Forward Message', () => {
const commentMessage = 'Comment for the forwarded message';
before(() => {
// # Testing Forwarding from Insights view requires a license
cy.apiRequireLicense();
cy.shouldHaveFeatureFlag('InsightsEnabled', true);
cy.apiUpdateConfig({
ServiceSettings: {
ThreadAutoFollow: true,

View File

@ -23,9 +23,7 @@ describe('Forward Message', () => {
const replyMessage = 'Forward this reply';
before(() => {
// # Testing Forwarding from Insights view requires a license
cy.apiRequireLicense();
cy.shouldHaveFeatureFlag('InsightsEnabled', true);
cy.apiUpdateConfig({
ServiceSettings: {

View File

@ -30,10 +30,6 @@ describe('Forward Message', () => {
const replyMessage = 'Forward this reply';
before(() => {
// # Testing Forwarding from Insights view requires a license
cy.apiRequireLicense();
cy.shouldHaveFeatureFlag('InsightsEnabled', true);
cy.apiUpdateConfig({
ServiceSettings: {
ThreadAutoFollow: true,
@ -201,32 +197,6 @@ describe('Forward Message', () => {
verifyForwardedMessage({post: replyPost});
});
it('MM-T4934_5 Forward public channel post while viewing Insights', () => {
// # Open the RHS with replies to the root post
cy.uiClickPostDropdownMenu(testPost.id, 'Reply', 'CENTER');
// * Assert RHS is open
cy.get('#rhsContainer').should('be.visible');
// # Visit global threads
cy.uiClickSidebarItem('insights');
// # Click on ... button of reply post
cy.clickPostDotMenu(replyPost.id, 'RHS_COMMENT');
// * Assert availability of the Forward menu-item
cy.findByText('Forward').click();
// * Forward Post
forwardPost({channelId: privateChannel.id});
// * Assert switch to testchannel
cy.get('#channelHeaderTitle', {timeout: TIMEOUTS.HALF_MIN}).should('be.visible').should('contain', privateChannel.display_name);
// * Assert post has been forwarded
verifyForwardedMessage({post: replyPost});
});
it('MM-T4934_6 Forward public channel post to Private channel', () => {
// # Check if ... button is visible in last post right side
cy.get(`#CENTER_button_${testPost.id}`).should('not.be.visible');

View File

@ -426,12 +426,6 @@ Cypress.Commands.add('apiDisableTutorials', (userId) => {
name: 'actions_menu_tutorial_state',
value: '{"actions_menu_modal_viewed":true}',
},
{
user_id: userId,
category: 'insights',
name: 'insights_tutorial_state',
value: '{"insights_modal_viewed":true}',
},
{
user_id: userId,
category: 'drafts',

View File

@ -116,11 +116,6 @@ function uiGetSidebarThreadsButton(): ChainableT<JQuery> {
}
Cypress.Commands.add('uiGetSidebarThreadsButton', uiGetSidebarThreadsButton);
function uiGetSidebarInsightsButton(): ChainableT<JQuery> {
return cy.get('#sidebar-insights-button').should('be.visible');
}
Cypress.Commands.add('uiGetSidebarInsightsButton', uiGetSidebarInsightsButton);
Cypress.Commands.add('uiGetChannelSidebarMenu', (channelName, isChannelId = false) => {
cy.uiGetLHS().within(() => {
if (isChannelId) {
@ -273,8 +268,6 @@ declare global {
uiGetSystemConsoleMenu: typeof uiGetSystemConsoleMenu;
uiGetSidebarThreadsButton: typeof uiGetSidebarThreadsButton;
uiGetSidebarInsightsButton: typeof uiGetSidebarInsightsButton;
}
}
}

View File

@ -672,7 +672,6 @@ const defaultServerConfig: AdminConfig = {
BoardsDataRetention: false,
NormalizeLdapDNs: false,
GraphQL: false,
InsightsEnabled: true,
CommandPalette: false,
SendWelcomePost: true,
PostPriority: true,

View File

@ -135,9 +135,6 @@ type Routes struct {
Permissions *mux.Router // 'api/v4/permissions'
InsightsForTeam *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}/top'
InsightsForUser *mux.Router // 'api/v4/users/me/top'
Usage *mux.Router // 'api/v4/usage'
HostedCustomer *mux.Router // 'api/v4/hosted_customer'
@ -264,9 +261,6 @@ func Init(srv *app.Server) (*API, error) {
api.BaseRoutes.Permissions = api.BaseRoutes.APIRoot.PathPrefix("/permissions").Subrouter()
api.BaseRoutes.InsightsForTeam = api.BaseRoutes.Team.PathPrefix("/top").Subrouter()
api.BaseRoutes.InsightsForUser = api.BaseRoutes.Users.PathPrefix("/me/top").Subrouter()
api.BaseRoutes.Usage = api.BaseRoutes.APIRoot.PathPrefix("/usage").Subrouter()
api.BaseRoutes.HostedCustomer = api.BaseRoutes.APIRoot.PathPrefix("/hosted_customer").Subrouter()
@ -314,7 +308,6 @@ func Init(srv *app.Server) (*API, error) {
api.InitSharedChannels()
api.InitPermissions()
api.InitExport()
api.InitInsights()
api.InitUsage()
api.InitHostedCustomer()
api.InitDrafts()

View File

@ -216,11 +216,3 @@ func minimumProfessionalLicense(c *Context) *model.AppError {
}
return nil
}
func rejectGuests(c *Context) *model.AppError {
if c.AppContext.Session().Props[model.SessionPropIsGuest] == "true" {
err := model.NewAppError("", model.NoTranslation, nil, "insufficient permissions as a guest user", http.StatusNotImplemented)
return err
}
return nil
}

View File

@ -1,637 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"time"
"github.com/mattermost/mattermost/server/public/model"
)
func (api *API) InitInsights() {
// Reactions
api.BaseRoutes.InsightsForTeam.Handle("/reactions", api.APISessionRequired(getTopReactionsForTeamSince)).Methods("GET")
api.BaseRoutes.InsightsForUser.Handle("/reactions", api.APISessionRequired(getTopReactionsForUserSince)).Methods("GET")
// Channels
api.BaseRoutes.InsightsForTeam.Handle("/channels", api.APISessionRequired(getTopChannelsForTeamSince)).Methods("GET")
api.BaseRoutes.InsightsForUser.Handle("/channels", api.APISessionRequired(getTopChannelsForUserSince)).Methods("GET")
// Threads
api.BaseRoutes.InsightsForTeam.Handle("/threads", api.APISessionRequired(getTopThreadsForTeamSince)).Methods("GET")
api.BaseRoutes.InsightsForUser.Handle("/threads", api.APISessionRequired(getTopThreadsForUserSince)).Methods("GET")
// user DMs
api.BaseRoutes.InsightsForUser.Handle("/dms", api.APISessionRequired(getTopDMsForUserSince)).Methods("GET")
// Inactive channels
api.BaseRoutes.InsightsForTeam.Handle("/inactive_channels", api.APISessionRequired(getTopInactiveChannelsForTeamSince)).Methods("GET")
api.BaseRoutes.InsightsForUser.Handle("/inactive_channels", api.APISessionRequired(getTopInactiveChannelsForUserSince)).Methods("GET")
// New teammembers
api.BaseRoutes.InsightsForTeam.Handle("/team_members", api.APISessionRequired(getNewTeamMembersSince)).Methods("GET")
}
// Top Reactions
func getTopReactionsForTeamSince(c *Context, w http.ResponseWriter, r *http.Request) {
// license and guest user check
permissionErr := minimumProfessionalLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
permissionErr = rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireTeamId()
if c.Err != nil {
return
}
team, appErr := c.App.GetTeam(c.Params.TeamId)
if appErr != nil {
c.Err = appErr
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
user, appErr := c.App.GetUser(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
if appErr != nil {
c.Err = appErr
return
}
topReactionList, appErr := c.App.GetTopReactionsForTeamSince(c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
if appErr != nil {
c.Err = appErr
return
}
if err := json.NewEncoder(w).Encode(topReactionList); err != nil {
c.Err = model.NewAppError("getTopReactionsForTeamSince", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}
func getTopReactionsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) {
// guest user check
permissionErr := rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.Params.TeamId = r.URL.Query().Get("team_id")
// TeamId is an optional parameter
if c.Params.TeamId != "" {
if !model.IsValidId(c.Params.TeamId) {
c.SetInvalidURLParam("team_id")
return
}
team, appErr := c.App.GetTeam(c.Params.TeamId)
if appErr != nil {
c.Err = appErr
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
}
user, appErr := c.App.GetUser(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
if appErr != nil {
c.Err = appErr
return
}
topReactionList, appErr := c.App.GetTopReactionsForUserSince(c.AppContext.Session().UserId, c.Params.TeamId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
if appErr != nil {
c.Err = appErr
return
}
if err := json.NewEncoder(w).Encode(topReactionList); err != nil {
c.Err = model.NewAppError("getTopReactionsForUserSince", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}
// Top Channels
func getTopChannelsForTeamSince(c *Context, w http.ResponseWriter, r *http.Request) {
// license and guest user check
permissionErr := minimumProfessionalLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
permissionErr = rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireTeamId()
if c.Err != nil {
return
}
team, appErr := c.App.GetTeam(c.Params.TeamId)
if appErr != nil {
c.Err = appErr
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
user, appErr := c.App.GetUser(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
loc := user.GetTimezoneLocation()
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, loc)
if appErr != nil {
c.Err = appErr
return
}
topChannels, appErr := c.App.GetTopChannelsForTeamSince(c.AppContext, c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
if appErr != nil {
c.Err = appErr
return
}
topChannels.PostCountByDuration, appErr = postCountByDurationViewModel(c, topChannels, startTime, c.Params.TimeRange, nil, loc)
if appErr != nil {
c.Err = appErr
return
}
if err := json.NewEncoder(w).Encode(topChannels); err != nil {
c.Err = model.NewAppError("getTopChannelsForTeamSince", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}
func getTopChannelsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) {
// guest user check
permissionErr := rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.Params.TeamId = r.URL.Query().Get("team_id")
// TeamId is an optional parameter
if c.Params.TeamId != "" {
if !model.IsValidId(c.Params.TeamId) {
c.SetInvalidURLParam("team_id")
return
}
team, appErr := c.App.GetTeam(c.Params.TeamId)
if appErr != nil {
c.Err = appErr
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
}
user, appErr := c.App.GetUser(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
loc := user.GetTimezoneLocation()
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, loc)
if appErr != nil {
c.Err = appErr
return
}
topChannels, appErr := c.App.GetTopChannelsForUserSince(c.AppContext, c.AppContext.Session().UserId, c.Params.TeamId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
if appErr != nil {
c.Err = appErr
return
}
topChannels.PostCountByDuration, appErr = postCountByDurationViewModel(c, topChannels, startTime, c.Params.TimeRange, &c.AppContext.Session().UserId, loc)
if appErr != nil {
c.Err = appErr
return
}
if err := json.NewEncoder(w).Encode(topChannels); err != nil {
c.Err = model.NewAppError("getTopChannelsForUserSince", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}
// Top Threads
func getTopThreadsForTeamSince(c *Context, w http.ResponseWriter, r *http.Request) {
// license and guest user check
permissionErr := minimumProfessionalLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
permissionErr = rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireTeamId()
if c.Err != nil {
return
}
team, appErr := c.App.GetTeam(c.Params.TeamId)
if appErr != nil {
c.Err = appErr
return
}
// restrict users with no access to team
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
if appErr != nil {
c.Err = appErr
return
}
topThreads, appErr := c.App.GetTopThreadsForTeamSince(c.AppContext, c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
if appErr != nil {
c.Err = appErr
return
}
if err := json.NewEncoder(w).Encode(topThreads); err != nil {
c.Err = model.NewAppError("getTopThreadsForTeamSince", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}
func getTopThreadsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) {
// guest user check
permissionErr := rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.Params.TeamId = r.URL.Query().Get("team_id")
// restrict users with no access to team
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
// TeamId is an optional parameter
if c.Params.TeamId != "" {
if !model.IsValidId(c.Params.TeamId) {
c.SetInvalidURLParam("team_id")
return
}
team, teamErr := c.App.GetTeam(c.Params.TeamId)
if teamErr != nil {
c.Err = teamErr
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
}
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
if appErr != nil {
c.Err = appErr
return
}
topThreads, appErr := c.App.GetTopThreadsForUserSince(c.AppContext, c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
if appErr != nil {
c.Err = appErr
return
}
if err := json.NewEncoder(w).Encode(topThreads); err != nil {
c.Err = model.NewAppError("getTopThreadsForUserSince", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}
// Top DMs
func getTopDMsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) {
// guest user check
permissionErr := rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
if appErr != nil {
c.Err = appErr
return
}
topDMs, err := c.App.GetTopDMsForUserSince(user.Id, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(topDMs); err != nil {
c.Err = model.NewAppError("getTopDMsForUserSince", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}
// Top Channels
func getTopInactiveChannelsForTeamSince(c *Context, w http.ResponseWriter, r *http.Request) {
// license and guest user check
permissionErr := minimumProfessionalLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
permissionErr = rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireTeamId()
if c.Err != nil {
return
}
team, err := c.App.GetTeam(c.Params.TeamId)
if err != nil {
c.Err = err
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
loc := user.GetTimezoneLocation()
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, loc)
if appErr != nil {
c.Err = appErr
return
}
topChannels, err := c.App.GetTopInactiveChannelsForTeamSince(c.AppContext, c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(topChannels); err != nil {
c.Err = model.NewAppError("getTopInactiveChannelsForTeamSince", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}
// top inactive channels
func getTopInactiveChannelsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) {
// guest user check
permissionErr := rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.Params.TeamId = r.URL.Query().Get("team_id")
// TeamId is an optional parameter
if c.Params.TeamId != "" {
if !model.IsValidId(c.Params.TeamId) {
c.SetInvalidURLParam("team_id")
return
}
team, teamErr := c.App.GetTeam(c.Params.TeamId)
if teamErr != nil {
c.Err = teamErr
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
}
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
loc := user.GetTimezoneLocation()
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, loc)
if appErr != nil {
c.Err = appErr
return
}
topChannels, err := c.App.GetTopInactiveChannelsForUserSince(c.AppContext, c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
if err != nil {
c.Err = err
return
}
if err := json.NewEncoder(w).Encode(topChannels); err != nil {
c.Err = model.NewAppError("getTopInactiveChannelsForUserSince", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}
// postCountByDurationViewModel expects a list of channels that are pre-authorized for the given user to view.
func postCountByDurationViewModel(c *Context, topChannelList *model.TopChannelList, startTime *time.Time, timeRange string, userID *string, location *time.Location) (model.ChannelPostCountByDuration, *model.AppError) {
if len(topChannelList.Items) == 0 {
return nil, nil
}
var postCountsByDay []*model.DurationPostCount
channelIDs := topChannelList.ChannelIDs()
var grouping model.PostCountGrouping
if timeRange == model.TimeRangeToday {
grouping = model.PostsByHour
} else {
grouping = model.PostsByDay
}
postCountsByDay, err := c.App.PostCountsByDuration(c.AppContext, channelIDs, startTime.UnixMilli(), userID, grouping, location)
if err != nil {
return nil, err
}
return model.ToDailyPostCountViewModel(postCountsByDay, startTime, model.TimeRangeToNumberDays(timeRange), channelIDs), nil
}
func getNewTeamMembersSince(c *Context, w http.ResponseWriter, r *http.Request) {
// license and guest user check
permissionErr := minimumProfessionalLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
permissionErr = rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireTeamId()
if c.Err != nil {
return
}
team, err := c.App.GetTeam(c.Params.TeamId)
if err != nil {
c.Err = err
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
loc := user.GetTimezoneLocation()
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, loc)
if appErr != nil {
c.Err = appErr
return
}
ntms, count, err := c.App.GetNewTeamMembersSince(c.AppContext, c.Params.TeamId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
if err != nil {
c.Err = err
return
}
ntms.TotalCount = count
if err := json.NewEncoder(w).Encode(ntms); err != nil {
c.Err = model.NewAppError("getNewTeamembersForTeamSince", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}

File diff suppressed because it is too large Load Diff

View File

@ -50,8 +50,8 @@ func TestGetPreferences(t *testing.T) {
prefs, _, err := client.GetPreferences(context.Background(), user1.Id)
require.NoError(t, err)
// 6 because we have 3 initial preferences insights, tutorial_step and recommended_next_steps added when creating a new user
require.Equal(t, len(prefs), 6, "received the wrong number of preferences")
// 5 because we have 2 initial preferences tutorial_step and recommended_next_steps added when creating a new user
require.Equal(t, len(prefs), 5, "received the wrong number of preferences")
for _, preference := range prefs {
require.Equal(t, preference.UserId, th.BasicUser.Id, "user id does not match")

View File

@ -285,12 +285,6 @@ type AppIface interface {
// PopulateWebConnConfig checks if the connection id already exists in the hub,
// and if so, accordingly populates the other fields of the webconn.
PopulateWebConnConfig(s *model.Session, cfg *platform.WebConnConfig, seqVal string) (*platform.WebConnConfig, error)
// PostCountsByDuration returns the post counts for the given channels, grouped by day, starting at the given time.
// Unless one is specifically itending to omit results from part of the calendar day, it will typically makes the most sense to
// use a sinceUnixMillis parameter value as returned by model.GetStartOfDayMillis.
//
// WARNING: PostCountsByDuration PERFORMS NO AUTHORIZATION CHECKS ON THE GIVEN CHANNELS.
PostCountsByDuration(c request.CTX, channelIDs []string, sinceUnixMillis int64, userID *string, grouping model.PostCountGrouping, groupingLocation *time.Location) ([]*model.DurationPostCount, *model.AppError)
// PromoteGuestToUser Convert user's roles and all his membership's roles from
// guest roles to regular user roles.
PromoteGuestToUser(c *request.Context, user *model.User, requestorId string) *model.AppError
@ -696,7 +690,6 @@ type AppIface interface {
GetMemberCountsByGroup(ctx context.Context, channelID string, includeTimezones bool) ([]*model.ChannelMemberCountByGroup, *model.AppError)
GetMessageForNotification(post *model.Post, translateFunc i18n.TranslateFunc) string
GetMultipleEmojiByName(c request.CTX, names []string) ([]*model.Emoji, *model.AppError)
GetNewTeamMembersSince(c request.CTX, teamID string, opts *model.InsightsOpts) (*model.NewTeamMembersList, int64, *model.AppError)
GetNewUsersForTeamPage(teamID string, page, perPage int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError)
GetNextPostIdFromPostList(postList *model.PostList, collapsedThreads bool) string
GetNotificationNameFormat(user *model.User) string
@ -817,15 +810,6 @@ type AppIface interface {
GetThreadMembershipsForUser(userID, teamID string) ([]*model.ThreadMembership, error)
GetThreadsForUser(userID, teamID string, options model.GetUserThreadsOpts) (*model.Threads, *model.AppError)
GetTokenById(token string) (*model.Token, *model.AppError)
GetTopChannelsForTeamSince(c request.CTX, teamID, userID string, opts *model.InsightsOpts) (*model.TopChannelList, *model.AppError)
GetTopChannelsForUserSince(c request.CTX, userID, teamID string, opts *model.InsightsOpts) (*model.TopChannelList, *model.AppError)
GetTopDMsForUserSince(userID string, opts *model.InsightsOpts) (*model.TopDMList, *model.AppError)
GetTopInactiveChannelsForTeamSince(c request.CTX, teamID, userID string, opts *model.InsightsOpts) (*model.TopInactiveChannelList, *model.AppError)
GetTopInactiveChannelsForUserSince(c request.CTX, teamID, userID string, opts *model.InsightsOpts) (*model.TopInactiveChannelList, *model.AppError)
GetTopReactionsForTeamSince(teamID string, userID string, opts *model.InsightsOpts) (*model.TopReactionList, *model.AppError)
GetTopReactionsForUserSince(userID string, teamID string, opts *model.InsightsOpts) (*model.TopReactionList, *model.AppError)
GetTopThreadsForTeamSince(c request.CTX, teamID, userID string, opts *model.InsightsOpts) (*model.TopThreadList, *model.AppError)
GetTopThreadsForUserSince(c request.CTX, teamID, userID string, opts *model.InsightsOpts) (*model.TopThreadList, *model.AppError)
GetTrueUpProfile() (map[string]any, error)
GetUploadSession(c request.CTX, uploadId string) (*model.UploadSession, *model.AppError)
GetUploadSessionsForUser(userID string) ([]*model.UploadSession, *model.AppError)

View File

@ -10,7 +10,6 @@ import (
"fmt"
"net/http"
"strings"
"time"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin"
@ -3518,66 +3517,3 @@ func (s *Server) getDirectChannel(c request.CTX, userID, otherUserID string) (*m
return channel, nil
}
func (a *App) GetTopChannelsForTeamSince(c request.CTX, teamID, userID string, opts *model.InsightsOpts) (*model.TopChannelList, *model.AppError) {
if !a.Config().FeatureFlags.InsightsEnabled {
return nil, model.NewAppError("GetTopChannelsForTeamSince", "api.insights.feature_disabled", nil, "", http.StatusNotImplemented)
}
topChannels, err := a.Srv().Store().Channel().GetTopChannelsForTeamSince(teamID, userID, opts.StartUnixMilli, opts.Page*opts.PerPage, opts.PerPage)
if err != nil {
return nil, model.NewAppError("GetTopChannelsForTeamSince", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
}
return topChannels, nil
}
func (a *App) GetTopChannelsForUserSince(c request.CTX, userID, teamID string, opts *model.InsightsOpts) (*model.TopChannelList, *model.AppError) {
if !a.Config().FeatureFlags.InsightsEnabled {
return nil, model.NewAppError("GetTopChannelsForUserSince", "api.insights.feature_disabled", nil, "", http.StatusNotImplemented)
}
topChannels, err := a.Srv().Store().Channel().GetTopChannelsForUserSince(userID, teamID, opts.StartUnixMilli, opts.Page*opts.PerPage, opts.PerPage)
if err != nil {
return nil, model.NewAppError("GetTopChannelsForUserSince", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
}
return topChannels, nil
}
// PostCountsByDuration returns the post counts for the given channels, grouped by day, starting at the given time.
// Unless one is specifically itending to omit results from part of the calendar day, it will typically makes the most sense to
// use a sinceUnixMillis parameter value as returned by model.GetStartOfDayMillis.
//
// WARNING: PostCountsByDuration PERFORMS NO AUTHORIZATION CHECKS ON THE GIVEN CHANNELS.
func (a *App) PostCountsByDuration(c request.CTX, channelIDs []string, sinceUnixMillis int64, userID *string, grouping model.PostCountGrouping, groupingLocation *time.Location) ([]*model.DurationPostCount, *model.AppError) {
if !a.Config().FeatureFlags.InsightsEnabled {
return nil, model.NewAppError("PostCountsByDuration", "api.insights.feature_disabled", nil, "", http.StatusNotImplemented)
}
postCountByDay, err := a.Srv().Store().Channel().PostCountsByDuration(channelIDs, sinceUnixMillis, userID, grouping, groupingLocation)
if err != nil {
return nil, model.NewAppError("PostCountsByDuration", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
}
return postCountByDay, nil
}
func (a *App) GetTopInactiveChannelsForTeamSince(c request.CTX, teamID, userID string, opts *model.InsightsOpts) (*model.TopInactiveChannelList, *model.AppError) {
if !a.Config().FeatureFlags.InsightsEnabled {
return nil, model.NewAppError("GetTopChannelsForTeamSince", "api.insights.feature_disabled", nil, "", http.StatusNotImplemented)
}
topChannels, err := a.Srv().Store().Channel().GetTopInactiveChannelsForTeamSince(teamID, userID, opts.StartUnixMilli, opts.Page*opts.PerPage, opts.PerPage)
if err != nil {
return nil, model.NewAppError("GetTopInactiveChannelsForTeamSince", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
}
return topChannels, nil
}
func (a *App) GetTopInactiveChannelsForUserSince(c request.CTX, teamID, userID string, opts *model.InsightsOpts) (*model.TopInactiveChannelList, *model.AppError) {
if !a.Config().FeatureFlags.InsightsEnabled {
return nil, model.NewAppError("GetTopChannelsForUserSince", "api.insights.feature_disabled", nil, "", http.StatusNotImplemented)
}
topChannels, err := a.Srv().Store().Channel().GetTopInactiveChannelsForUserSince(teamID, userID, opts.StartUnixMilli, opts.Page*opts.PerPage, opts.PerPage)
if err != nil {
return nil, model.NewAppError("GetTopInactiveChannelsForUserSince", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
}
return topChannels, nil
}

View File

@ -12,7 +12,6 @@ import (
"strings"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
@ -2495,523 +2494,3 @@ func TestIsCRTEnabledForUser(t *testing.T) {
})
}
}
func TestGetTopChannelsForTeamSince(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.ConfigStore.SetReadOnlyFF(false)
defer th.ConfigStore.SetReadOnlyFF(true)
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.InsightsEnabled = true })
channel2 := th.CreateChannel(th.Context, th.BasicTeam)
// add a bot post to ensure it's not counted
_, err := th.Server.Store().Post().Save(&model.Post{
Message: "hello from a bot",
ChannelId: channel2.Id,
UserId: th.BasicUser.Id,
Props: model.StringInterface{
"from_bot": true,
},
})
require.NoError(t, err)
channel3 := th.CreatePrivateChannel(th.Context, th.BasicTeam)
// add a webhook post to ensure it's not counted
_, err = th.Server.Store().Post().Save(&model.Post{
Message: "hello from a webhook",
ChannelId: channel3.Id,
UserId: th.BasicUser.Id,
Props: model.StringInterface{
"from_webhook": true,
},
})
require.NoError(t, err)
// add an oauth app post to ensure it's not counted
_, err = th.Server.Store().Post().Save(&model.Post{
Message: "hello from an ouath app",
ChannelId: channel3.Id,
UserId: th.BasicUser.Id,
Props: model.StringInterface{
"from_oauth_app": true,
},
})
require.NoError(t, err)
// add a plugin post to ensure it's not counted
_, err = th.Server.Store().Post().Save(&model.Post{
Message: "hello from a plugin",
ChannelId: channel3.Id,
UserId: th.BasicUser.Id,
Props: model.StringInterface{
"from_plugin": true,
},
})
require.NoError(t, err)
// add a system post to ensure it's not counted
_, err = th.Server.Store().Post().Save(&model.Post{
Message: "system message",
Type: "system_join_channel",
ChannelId: channel3.Id,
UserId: th.BasicUser.Id,
Props: model.StringInterface{
"from_oauth_app": true,
},
})
require.NoError(t, err)
channel4 := th.CreatePrivateChannel(th.Context, th.BasicTeam)
channel5 := th.CreateChannel(th.Context, th.BasicTeam)
channel6 := th.CreatePrivateChannel(th.Context, th.BasicTeam)
th.AddUserToChannel(th.BasicUser, channel2)
th.AddUserToChannel(th.BasicUser, channel3)
th.AddUserToChannel(th.BasicUser, channel4)
th.AddUserToChannel(th.BasicUser, channel5)
th.AddUserToChannel(th.BasicUser, channel6)
channels := [6]*model.Channel{th.BasicChannel, channel2, channel3, channel4, channel5, channel6}
i := len(channels)
for _, channel := range channels {
for j := i; j > 0; j-- {
th.CreatePost(channel)
}
i--
}
expectedTopChannels := []struct {
ID string
MessageCount int64
}{
{ID: th.BasicChannel.Id, MessageCount: 7},
{ID: channel2.Id, MessageCount: 5},
{ID: channel3.Id, MessageCount: 4},
{ID: channel4.Id, MessageCount: 3},
{ID: channel5.Id, MessageCount: 2},
}
timeRange, _ := model.GetStartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location())
t.Run("get-top-channels-for-team-since", func(t *testing.T) {
topChannels, err := th.App.GetTopChannelsForTeamSince(th.Context, th.BasicChannel.TeamId, th.BasicUser.Id, &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 0, PerPage: 5})
require.Nil(t, err)
for i, channel := range topChannels.Items {
assert.Equal(t, expectedTopChannels[i].ID, channel.ID)
assert.Equal(t, expectedTopChannels[i].MessageCount, channel.MessageCount)
}
topChannels, err = th.App.GetTopChannelsForTeamSince(th.Context, th.BasicChannel.TeamId, th.BasicUser.Id, &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 1, PerPage: 5})
require.Nil(t, err)
assert.Equal(t, channel6.Id, topChannels.Items[0].ID)
assert.Equal(t, int64(1), topChannels.Items[0].MessageCount)
})
}
func TestGetTopChannelsForUserSince(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.ConfigStore.SetReadOnlyFF(false)
defer th.ConfigStore.SetReadOnlyFF(true)
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.InsightsEnabled = true })
channel2 := th.CreateChannel(th.Context, th.BasicTeam)
// add a bot post to ensure it's not counted
_, err := th.Server.Store().Post().Save(&model.Post{
Message: "hello from a bot",
ChannelId: channel2.Id,
UserId: th.BasicUser.Id,
Props: model.StringInterface{
"from_bot": true,
},
})
require.NoError(t, err)
channel3 := th.CreatePrivateChannel(th.Context, th.BasicTeam)
// add a webhook post to ensure it's not counted
_, err = th.Server.Store().Post().Save(&model.Post{
Message: "hello from a webhook",
ChannelId: channel3.Id,
UserId: th.BasicUser.Id,
Props: model.StringInterface{
"from_webhook": true,
},
})
require.NoError(t, err)
channel4 := th.CreatePrivateChannel(th.Context, th.BasicTeam)
channel5 := th.CreateChannel(th.Context, th.BasicTeam)
channel6 := th.CreatePrivateChannel(th.Context, th.BasicTeam)
th.AddUserToChannel(th.BasicUser, channel2)
th.AddUserToChannel(th.BasicUser, channel3)
th.AddUserToChannel(th.BasicUser, channel4)
th.AddUserToChannel(th.BasicUser, channel5)
th.AddUserToChannel(th.BasicUser, channel6)
channels := [6]*model.Channel{th.BasicChannel, channel2, channel3, channel4, channel5, channel6}
i := len(channels)
for _, channel := range channels {
for j := i; j > 0; j-- {
th.CreatePost(channel)
}
i--
}
expectedTopChannels := []struct {
ID string
MessageCount int64
}{
{ID: th.BasicChannel.Id, MessageCount: 7},
{ID: channel2.Id, MessageCount: 5},
{ID: channel3.Id, MessageCount: 4},
{ID: channel4.Id, MessageCount: 3},
{ID: channel5.Id, MessageCount: 2},
}
timeRange, _ := model.GetStartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location())
t.Run("get-top-channels-for-user-since", func(t *testing.T) {
topChannels, err := th.App.GetTopChannelsForUserSince(th.Context, th.BasicUser.Id, "", &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 0, PerPage: 5})
require.Nil(t, err)
for i, channel := range topChannels.Items {
assert.Equal(t, expectedTopChannels[i].ID, channel.ID)
assert.Equal(t, expectedTopChannels[i].MessageCount, channel.MessageCount)
}
topChannels, err = th.App.GetTopChannelsForUserSince(th.Context, th.BasicUser.Id, th.BasicChannel.TeamId, &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 1, PerPage: 5})
require.Nil(t, err)
assert.Equal(t, channel6.Id, topChannels.Items[0].ID)
assert.Equal(t, int64(1), topChannels.Items[0].MessageCount)
})
}
func TestPostCountsByDuration(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.ConfigStore.SetReadOnlyFF(false)
defer th.ConfigStore.SetReadOnlyFF(true)
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.InsightsEnabled = true })
channel2 := th.CreateChannel(th.Context, th.BasicTeam)
channel3 := th.CreatePrivateChannel(th.Context, th.BasicTeam)
channel4 := th.CreatePrivateChannel(th.Context, th.BasicTeam)
channel5 := th.CreateChannel(th.Context, th.BasicTeam)
channel6 := th.CreatePrivateChannel(th.Context, th.BasicTeam)
defer func() {
th.App.PermanentDeleteChannel(th.Context, channel2)
th.App.PermanentDeleteChannel(th.Context, channel3)
th.App.PermanentDeleteChannel(th.Context, channel4)
th.App.PermanentDeleteChannel(th.Context, channel5)
th.App.PermanentDeleteChannel(th.Context, channel6)
}()
th.AddUserToChannel(th.BasicUser, channel2)
th.AddUserToChannel(th.BasicUser, channel3)
th.AddUserToChannel(th.BasicUser, channel4)
th.AddUserToChannel(th.BasicUser, channel5)
th.AddUserToChannel(th.BasicUser, channel6)
channels := [6]*model.Channel{th.BasicChannel, channel2, channel3, channel4, channel5, channel6}
channelIDs := []string{th.BasicChannel.Id, channel2.Id, channel3.Id, channel4.Id, channel5.Id, channel6.Id}
i := len(channels)
for ci, channel := range channels {
for j := i; j > 0; j-- {
d1 := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).AddDate(0, 0, ci)
d2 := time.Date(2009, time.November, 10, 9, 0, 0, 0, time.UTC).AddDate(0, 0, ci)
_, err := th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: channel.Id,
CreateAt: d1.Unix() * 1000,
}, channel, false, false)
require.Nil(t, err)
_, err = th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser2.Id,
ChannelId: channel.Id,
CreateAt: d2.Unix() * 1000,
}, channel, false, false)
require.Nil(t, err)
}
i--
}
expectedDayGrouping := map[string]map[string]int{
"2009-11-10": {
th.BasicChannel.Id: 6,
},
"2009-11-11": {
channel2.Id: 5,
},
"2009-11-12": {
channel3.Id: 4,
},
"2009-11-13": {
channel4.Id: 3,
},
"2009-11-14": {
channel5.Id: 2,
},
"2009-11-15": {
channel6.Id: 1,
},
}
expectedHourGrouping := map[string]map[string]int{
"2009-11-15T09": {
channel6.Id: 1,
},
"2009-11-15T23": {
channel6.Id: 1,
},
}
sinceUnixMillis := time.Date(2009, time.November, 9, 23, 0, 0, 0, time.UTC).UnixMilli()
t.Run("get-post-counts-by-day scoped by user, grouped by day", func(t *testing.T) {
dailyPostCount, err := th.App.PostCountsByDuration(th.Context, channelIDs, sinceUnixMillis, &th.BasicUser.Id, model.PostsByDay, time.Now().UTC().Location())
require.Nil(t, err)
require.GreaterOrEqual(t, len(dailyPostCount), 6)
for _, item := range dailyPostCount {
if strings.HasPrefix(item.Duration, "2009") {
expectedCount := expectedDayGrouping[item.Duration][item.ChannelID]
assert.Equal(t, expectedCount, item.PostCount)
}
}
})
t.Run("get-post-counts-by-day all users, grouped by day", func(t *testing.T) {
dailyPostCount, err := th.App.PostCountsByDuration(th.Context, channelIDs, sinceUnixMillis, nil, model.PostsByDay, time.Now().UTC().Location())
require.Nil(t, err)
require.GreaterOrEqual(t, len(dailyPostCount), 6)
for _, item := range dailyPostCount {
if strings.HasPrefix(item.Duration, "2009") {
expectedCount := expectedDayGrouping[item.Duration][item.ChannelID]
assert.Equal(t, expectedCount*2, item.PostCount)
}
}
})
t.Run("get-post-counts-by-day all users, grouped by hour", func(t *testing.T) {
oneDaySince := time.Date(2009, time.November, 14, 23, 0, 0, 0, time.UTC).UnixMilli()
dailyPostCount, err := th.App.PostCountsByDuration(th.Context, channelIDs, oneDaySince, nil, model.PostsByHour, time.Now().UTC().Location())
require.Nil(t, err)
require.GreaterOrEqual(t, len(dailyPostCount), 1)
for _, item := range dailyPostCount {
if strings.HasPrefix(item.Duration, "2009") {
expectedCount := expectedHourGrouping[item.Duration][item.ChannelID]
assert.Equal(t, expectedCount, item.PostCount)
}
}
})
}
// Top inactive channels
func TestGetTopInactiveChannelsForTeamSince(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.ConfigStore.SetReadOnlyFF(false)
defer th.ConfigStore.SetReadOnlyFF(true)
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.InsightsEnabled = true })
channel2 := th.CreateChannel(th.Context, th.BasicTeam, WithCreateAt(1))
channel3 := th.CreateChannel(th.Context, th.BasicTeam, WithCreateAt(1))
channel4 := th.CreatePrivateChannel(th.Context, th.BasicTeam, WithCreateAt(1))
channel5 := th.CreateChannel(th.Context, th.BasicTeam, WithCreateAt(1))
channel6 := th.CreatePrivateChannel(th.Context, th.BasicTeam, WithCreateAt(1))
th.AddUserToChannel(th.BasicUser, channel2)
th.AddUserToChannel(th.BasicUser, channel3)
th.AddUserToChannel(th.BasicUser, channel4)
th.AddUserToChannel(th.BasicUser, channel5)
th.AddUserToChannel(th.BasicUser, channel6)
// delete offtopic, town square, basicChannel channel - which interferes with 'least' active channel results
offTopicChannel, appErr := th.App.GetChannelByName(th.Context, "off-topic", th.BasicTeam.Id, false)
require.Nil(t, appErr, "Expected nil, didn't receive nil")
appErr = th.App.PermanentDeleteChannel(th.Context, offTopicChannel)
require.Nil(t, appErr)
townSquareChannel, appErr := th.App.GetChannelByName(th.Context, "town-square", th.BasicTeam.Id, false)
require.Nil(t, appErr, "Expected nil, didn't receive nil")
appErr = th.App.PermanentDeleteChannel(th.Context, townSquareChannel)
require.Nil(t, appErr)
basicChannel, appErr := th.App.GetChannel(th.Context, th.BasicChannel.Id)
require.Nil(t, appErr, "Expected nil, didn't receive nil")
appErr = th.App.PermanentDeleteChannel(th.Context, basicChannel)
require.Nil(t, appErr)
// add a bot post to ensure it's counted
_, err := th.Server.Store().Post().Save(&model.Post{
Message: "hello from a bot",
ChannelId: channel2.Id,
UserId: th.BasicUser.Id,
Props: model.StringInterface{
"from_bot": true,
},
})
require.NoError(t, err)
// add a webhook post to ensure it's counted
_, err = th.Server.Store().Post().Save(&model.Post{
Message: "hello from a webhook",
ChannelId: channel3.Id,
UserId: th.BasicUser.Id,
Props: model.StringInterface{
"from_webhook": true,
},
})
require.NoError(t, err)
channels := [5]*model.Channel{channel2, channel3, channel4, channel5, channel6}
i := len(channels)
for _, channel := range channels {
for j := i; j > 0; j-- {
th.CreatePost(channel)
}
i--
}
expectedTopChannels := []struct {
ID string
MessageCount int64
}{
{ID: channel6.Id, MessageCount: 1},
{ID: channel5.Id, MessageCount: 2},
{ID: channel4.Id, MessageCount: 3},
{ID: channel3.Id, MessageCount: 5},
{ID: channel2.Id, MessageCount: 6},
}
timeRange, _ := model.GetStartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location())
t.Run("get-top-channels-for-team-since", func(t *testing.T) {
topChannels, err := th.App.GetTopInactiveChannelsForTeamSince(th.Context, th.BasicChannel.TeamId, th.BasicUser.Id, &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 0, PerPage: 5})
require.Nil(t, err)
for i, channel := range topChannels.Items {
assert.Equal(t, expectedTopChannels[i].ID, channel.ID)
assert.Equal(t, expectedTopChannels[i].MessageCount, channel.MessageCount)
}
topChannels, err = th.App.GetTopInactiveChannelsForTeamSince(th.Context, th.BasicChannel.TeamId, th.BasicUser.Id, &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 1, PerPage: 4})
require.Nil(t, err)
assert.Equal(t, channel2.Id, topChannels.Items[0].ID)
assert.Equal(t, int64(6), topChannels.Items[0].MessageCount)
// it simulates channel being created recently
_ = th.CreatePrivateChannel(th.Context, th.BasicTeam)
topChannels, err = th.App.GetTopInactiveChannelsForTeamSince(th.Context, th.BasicChannel.TeamId, th.BasicUser.Id, &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 0, PerPage: 6})
require.Nil(t, err)
assert.Equal(t, 5, len(topChannels.Items))
})
}
func TestGetTopInactiveChannelsForUserSince(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.ConfigStore.SetReadOnlyFF(false)
defer th.ConfigStore.SetReadOnlyFF(true)
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.InsightsEnabled = true })
// delete offtopic, town-square, th.basicchannel channels - which interferes with 'least' active channel results
offTopicChannel, appErr := th.App.GetChannelByName(th.Context, "off-topic", th.BasicTeam.Id, false)
require.Nil(t, appErr, "Expected nil, didn't receive nil")
appErr = th.App.PermanentDeleteChannel(th.Context, offTopicChannel)
require.Nil(t, appErr)
townSquareChannel, appErr := th.App.GetChannelByName(th.Context, "town-square", th.BasicTeam.Id, false)
require.Nil(t, appErr, "Expected nil, didn't receive nil")
appErr = th.App.PermanentDeleteChannel(th.Context, townSquareChannel)
require.Nil(t, appErr)
basicChannel, appErr := th.App.GetChannel(th.Context, th.BasicChannel.Id)
require.Nil(t, appErr, "Expected nil, didn't receive nil")
appErr = th.App.PermanentDeleteChannel(th.Context, basicChannel)
require.Nil(t, appErr)
channel2 := th.CreateChannel(th.Context, th.BasicTeam, WithCreateAt(1))
// add a bot post to ensure it's counted
_, err := th.Server.Store().Post().Save(&model.Post{
Message: "hello from a bot",
ChannelId: channel2.Id,
UserId: th.BasicUser.Id,
Props: model.StringInterface{
"from_bot": true,
},
})
require.NoError(t, err)
channel3 := th.CreatePrivateChannel(th.Context, th.BasicTeam, WithCreateAt(1))
// add a webhook post to ensure it's counted
_, err = th.Server.Store().Post().Save(&model.Post{
Message: "hello from a webhook",
ChannelId: channel3.Id,
UserId: th.BasicUser.Id,
Props: model.StringInterface{
"from_webhook": true,
},
})
require.NoError(t, err)
channel4 := th.CreatePrivateChannel(th.Context, th.BasicTeam, WithCreateAt(1))
channel5 := th.CreateChannel(th.Context, th.BasicTeam, WithCreateAt(1))
channel6 := th.CreatePrivateChannel(th.Context, th.BasicTeam, WithCreateAt(1))
th.AddUserToChannel(th.BasicUser, channel2)
th.AddUserToChannel(th.BasicUser, channel3)
th.AddUserToChannel(th.BasicUser, channel4)
th.AddUserToChannel(th.BasicUser, channel5)
th.AddUserToChannel(th.BasicUser, channel6)
channels := [5]*model.Channel{channel2, channel3, channel4, channel5, channel6}
i := len(channels)
for _, channel := range channels {
for j := i; j > 0; j-- {
th.CreatePost(channel)
}
i--
}
expectedTopChannels := []struct {
ID string
MessageCount int64
}{
{ID: channel6.Id, MessageCount: 1},
{ID: channel5.Id, MessageCount: 2},
{ID: channel4.Id, MessageCount: 3},
{ID: channel3.Id, MessageCount: 5},
{ID: channel2.Id, MessageCount: 6},
}
timeRange, _ := model.GetStartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location())
t.Run("get-top-channels-for-user-since", func(t *testing.T) {
topChannels, err := th.App.GetTopInactiveChannelsForUserSince(th.Context, th.BasicChannel.TeamId, th.BasicUser.Id, &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 0, PerPage: 4})
require.Nil(t, err)
require.Equal(t, len(topChannels.Items), 4)
for i, channel := range topChannels.Items {
assert.Equal(t, expectedTopChannels[i].ID, channel.ID)
assert.Equal(t, expectedTopChannels[i].MessageCount, channel.MessageCount)
}
topChannels, err = th.App.GetTopInactiveChannelsForUserSince(th.Context, th.BasicChannel.TeamId, th.BasicUser.Id, &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 1, PerPage: 4})
require.Nil(t, err)
require.Equal(t, len(topChannels.Items), 1)
assert.Equal(t, channel2.Id, topChannels.Items[0].ID)
assert.Equal(t, int64(6), topChannels.Items[0].MessageCount)
})
}

View File

@ -7381,28 +7381,6 @@ func (a *OpenTracingAppLayer) GetMultipleEmojiByName(c request.CTX, names []stri
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetNewTeamMembersSince(c request.CTX, teamID string, opts *model.InsightsOpts) (*model.NewTeamMembersList, int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetNewTeamMembersSince")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1, resultVar2 := a.app.GetNewTeamMembersSince(c, teamID, opts)
if resultVar2 != nil {
span.LogFields(spanlog.Error(resultVar2))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1, resultVar2
}
func (a *OpenTracingAppLayer) GetNewUsersForTeamPage(teamID string, page int, perPage int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetNewUsersForTeamPage")
@ -10291,204 +10269,6 @@ func (a *OpenTracingAppLayer) GetTokenById(token string) (*model.Token, *model.A
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTopChannelsForTeamSince(c request.CTX, teamID string, userID string, opts *model.InsightsOpts) (*model.TopChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTopChannelsForTeamSince")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTopChannelsForTeamSince(c, teamID, userID, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTopChannelsForUserSince(c request.CTX, userID string, teamID string, opts *model.InsightsOpts) (*model.TopChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTopChannelsForUserSince")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTopChannelsForUserSince(c, userID, teamID, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTopDMsForUserSince(userID string, opts *model.InsightsOpts) (*model.TopDMList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTopDMsForUserSince")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTopDMsForUserSince(userID, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTopInactiveChannelsForTeamSince(c request.CTX, teamID string, userID string, opts *model.InsightsOpts) (*model.TopInactiveChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTopInactiveChannelsForTeamSince")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTopInactiveChannelsForTeamSince(c, teamID, userID, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTopInactiveChannelsForUserSince(c request.CTX, teamID string, userID string, opts *model.InsightsOpts) (*model.TopInactiveChannelList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTopInactiveChannelsForUserSince")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTopInactiveChannelsForUserSince(c, teamID, userID, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTopReactionsForTeamSince(teamID string, userID string, opts *model.InsightsOpts) (*model.TopReactionList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTopReactionsForTeamSince")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTopReactionsForTeamSince(teamID, userID, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTopReactionsForUserSince(userID string, teamID string, opts *model.InsightsOpts) (*model.TopReactionList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTopReactionsForUserSince")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTopReactionsForUserSince(userID, teamID, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTopThreadsForTeamSince(c request.CTX, teamID string, userID string, opts *model.InsightsOpts) (*model.TopThreadList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTopThreadsForTeamSince")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTopThreadsForTeamSince(c, teamID, userID, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTopThreadsForUserSince(c request.CTX, teamID string, userID string, opts *model.InsightsOpts) (*model.TopThreadList, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTopThreadsForUserSince")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetTopThreadsForUserSince(c, teamID, userID, opts)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetTotalUsersStats(viewRestrictions *model.ViewUsersRestrictions) (*model.UsersStats, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetTotalUsersStats")
@ -13413,28 +13193,6 @@ func (a *OpenTracingAppLayer) PostAddToChannelMessage(c request.CTX, user *model
return resultVar0
}
func (a *OpenTracingAppLayer) PostCountsByDuration(c request.CTX, channelIDs []string, sinceUnixMillis int64, userID *string, grouping model.PostCountGrouping, groupingLocation *time.Location) ([]*model.DurationPostCount, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PostCountsByDuration")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.PostCountsByDuration(c, channelIDs, sinceUnixMillis, userID, grouping, groupingLocation)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) PostPatchWithProxyRemovedFromImageURLs(patch *model.PostPatch) *model.PostPatch {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PostPatchWithProxyRemovedFromImageURLs")

View File

@ -178,22 +178,17 @@ func TestPluginAPIGetUserPreferences(t *testing.T) {
preferences, err := api.GetPreferencesForUser(user1.Id)
require.Nil(t, err)
assert.Equal(t, 3, len(preferences))
assert.Equal(t, 2, len(preferences))
assert.Equal(t, user1.Id, preferences[0].UserId)
assert.Equal(t, model.PreferenceCategoryInsights, preferences[0].Category)
assert.Equal(t, model.PreferenceNameInsights, preferences[0].Name)
assert.Equal(t, "{\"insights_modal_viewed\":false}", preferences[0].Value)
assert.Equal(t, model.PreferenceRecommendedNextSteps, preferences[0].Category)
assert.Equal(t, "hide", preferences[0].Name)
assert.Equal(t, "false", preferences[0].Value)
assert.Equal(t, user1.Id, preferences[1].UserId)
assert.Equal(t, model.PreferenceRecommendedNextSteps, preferences[1].Category)
assert.Equal(t, "hide", preferences[1].Name)
assert.Equal(t, "false", preferences[1].Value)
assert.Equal(t, user1.Id, preferences[2].UserId)
assert.Equal(t, model.PreferenceCategoryTutorialSteps, preferences[2].Category)
assert.Equal(t, user1.Id, preferences[2].Name)
assert.Equal(t, "0", preferences[2].Value)
assert.Equal(t, model.PreferenceCategoryTutorialSteps, preferences[1].Category)
assert.Equal(t, user1.Id, preferences[1].Name)
assert.Equal(t, "0", preferences[1].Value)
}
func TestPluginAPIDeleteUserPreferences(t *testing.T) {
@ -245,10 +240,9 @@ func TestPluginAPIDeleteUserPreferences(t *testing.T) {
require.Nil(t, err)
preferences, err = api.GetPreferencesForUser(user2.Id)
require.Nil(t, err)
assert.Equal(t, 3, len(preferences))
assert.Equal(t, model.PreferenceCategoryInsights, preferences[0].Category)
assert.Equal(t, model.PreferenceRecommendedNextSteps, preferences[1].Category)
assert.Equal(t, model.PreferenceCategoryTutorialSteps, preferences[2].Category)
assert.Equal(t, 2, len(preferences))
assert.Equal(t, model.PreferenceRecommendedNextSteps, preferences[0].Category)
assert.Equal(t, model.PreferenceCategoryTutorialSteps, preferences[1].Category)
}
func TestPluginAPIUpdateUserPreferences(t *testing.T) {
@ -266,22 +260,16 @@ func TestPluginAPIUpdateUserPreferences(t *testing.T) {
preferences, err := api.GetPreferencesForUser(user1.Id)
require.Nil(t, err)
assert.Equal(t, 3, len(preferences))
assert.Equal(t, 2, len(preferences))
assert.Equal(t, user1.Id, preferences[0].UserId)
assert.Equal(t, model.PreferenceCategoryInsights, preferences[0].Category)
assert.Equal(t, model.PreferenceNameInsights, preferences[0].Name)
assert.Equal(t, "{\"insights_modal_viewed\":false}", preferences[0].Value)
assert.Equal(t, model.PreferenceRecommendedNextSteps, preferences[0].Category)
assert.Equal(t, "hide", preferences[0].Name)
assert.Equal(t, "false", preferences[0].Value)
assert.Equal(t, user1.Id, preferences[1].UserId)
assert.Equal(t, model.PreferenceRecommendedNextSteps, preferences[1].Category)
assert.Equal(t, "hide", preferences[1].Name)
assert.Equal(t, "false", preferences[1].Value)
assert.Equal(t, user1.Id, preferences[2].UserId)
assert.Equal(t, model.PreferenceCategoryTutorialSteps, preferences[2].Category)
assert.Equal(t, user1.Id, preferences[2].Name)
assert.Equal(t, "0", preferences[2].Value)
assert.Equal(t, model.PreferenceCategoryTutorialSteps, preferences[1].Category)
assert.Equal(t, user1.Id, preferences[1].Name)
assert.Equal(t, "0", preferences[1].Value)
preference := model.Preference{
Name: user1.Id,
@ -296,8 +284,8 @@ func TestPluginAPIUpdateUserPreferences(t *testing.T) {
preferences, err = api.GetPreferencesForUser(user1.Id)
require.Nil(t, err)
assert.Equal(t, 4, len(preferences))
expectedCategories := []string{model.PreferenceCategoryTutorialSteps, model.PreferenceCategoryTheme, model.PreferenceRecommendedNextSteps, model.PreferenceCategoryInsights}
assert.Equal(t, 3, len(preferences))
expectedCategories := []string{model.PreferenceCategoryTutorialSteps, model.PreferenceCategoryTheme, model.PreferenceRecommendedNextSteps}
for _, pref := range preferences {
assert.Contains(t, expectedCategories, pref.Category)
assert.Equal(t, user1.Id, pref.UserId)

View File

@ -2045,49 +2045,6 @@ func (a *App) GetEditHistoryForPost(postID string) ([]*model.Post, *model.AppErr
return posts, nil
}
func (a *App) GetTopThreadsForTeamSince(c request.CTX, teamID, userID string, opts *model.InsightsOpts) (*model.TopThreadList, *model.AppError) {
if !a.Config().FeatureFlags.InsightsEnabled {
return nil, model.NewAppError("GetTopChannelsForTeamSince", "app.insights.feature_disabled", nil, "", http.StatusNotImplemented)
}
topThreads, err := a.Srv().Store().Thread().GetTopThreadsForTeamSince(teamID, userID, opts.StartUnixMilli, opts.Page*opts.PerPage, opts.PerPage)
if err != nil {
return nil, model.NewAppError("GetTopChannelsForTeamSince", "app.post.get_top_threads_for_team_since.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
topThreadsWithEmbedAndImage, err := includeEmbedsAndImages(a, c, topThreads, userID)
if err != nil {
return nil, model.NewAppError("GetTopChannelsForTeamSince", "app.post.get_top_threads_for_team_since.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return topThreadsWithEmbedAndImage, nil
}
func (a *App) GetTopThreadsForUserSince(c request.CTX, teamID, userID string, opts *model.InsightsOpts) (*model.TopThreadList, *model.AppError) {
if !a.Config().FeatureFlags.InsightsEnabled {
return nil, model.NewAppError("GetTopChannelsForTeamSince", "app.insights.feature_disabled", nil, "", http.StatusNotImplemented)
}
topThreads, err := a.Srv().Store().Thread().GetTopThreadsForUserSince(teamID, userID, opts.StartUnixMilli, opts.Page*opts.PerPage, opts.PerPage)
if err != nil {
return nil, model.NewAppError("GetTopChannelsForTeamSince", "app.post.get_top_threads_for_team_since.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
topThreadsWithEmbedAndImage, err := includeEmbedsAndImages(a, c, topThreads, userID)
if err != nil {
return nil, model.NewAppError("GetTopChannelsForUserSince", "app.post.get_top_threads_for_user_since.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return topThreadsWithEmbedAndImage, nil
}
func (a *App) GetTopDMsForUserSince(userID string, opts *model.InsightsOpts) (*model.TopDMList, *model.AppError) {
if !a.Config().FeatureFlags.InsightsEnabled {
return nil, model.NewAppError("GetTopDMsForUserSince", "app.insights.feature_disabled", nil, "", http.StatusNotImplemented)
}
topDMs, err := a.Srv().Store().Post().GetTopDMsForUserSince(userID, opts.StartUnixMilli, opts.Page*opts.PerPage, opts.PerPage)
if err != nil {
return nil, model.NewAppError("GetTopDMsForUserSince", "app.post.get_top_dms_for_user_since.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return topDMs, nil
}
func (a *App) SetPostReminder(postID, userID string, targetTime int64) *model.AppError {
// Store the reminder in the DB
reminder := &model.PostReminder{
@ -2285,15 +2242,3 @@ func (a *App) GetPostInfo(c request.CTX, postID string) (*model.PostInfo, *model
}
return &info, nil
}
func includeEmbedsAndImages(a *App, c request.CTX, topThreadList *model.TopThreadList, userID string) (*model.TopThreadList, error) {
for _, topThread := range topThreadList.Items {
topThread.Post = a.PreparePostForClientWithEmbedsAndImages(c, topThread.Post, false, false, true)
sanitizedPost, err := a.SanitizePostMetadataForUser(c, topThread.Post, userID)
if err != nil {
return nil, err
}
topThread.Post = sanitizedPost
}
return topThreadList, nil
}

View File

@ -3028,195 +3028,6 @@ func TestComputeLastAccessiblePostTime(t *testing.T) {
})
}
func TestGetTopThreadsForTeamSince(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.ConfigStore.SetReadOnlyFF(false)
defer th.ConfigStore.SetReadOnlyFF(true)
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.InsightsEnabled = true })
// create a public channel, a private channel
channelPublic := th.CreateChannel(th.Context, th.BasicTeam)
channelPrivate := th.CreatePrivateChannel(th.Context, th.BasicTeam)
th.AddUserToChannel(th.BasicUser, channelPublic)
th.AddUserToChannel(th.BasicUser, channelPrivate)
th.AddUserToChannel(th.BasicUser2, channelPublic)
// create two threads: one in public channel, one in private with only basicUser1
rootPostPublicChannel, appErr := th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: channelPublic.Id,
Message: "root post",
}, channelPublic, false, true)
require.Nil(t, appErr)
_, appErr = th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser2.Id,
ChannelId: channelPublic.Id,
RootId: rootPostPublicChannel.Id,
Message: fmt.Sprintf("@%s", th.BasicUser2.Username),
}, channelPublic, false, true)
require.Nil(t, appErr)
rootPostPrivateChannel, appErr := th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: channelPrivate.Id,
Message: "root post",
}, channelPrivate, false, true)
require.Nil(t, appErr)
_, appErr = th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: channelPrivate.Id,
RootId: rootPostPrivateChannel.Id,
Message: fmt.Sprintf("@%s", th.BasicUser2.Username),
}, channelPrivate, false, true)
require.Nil(t, appErr)
_, appErr = th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: channelPrivate.Id,
RootId: rootPostPrivateChannel.Id,
Message: fmt.Sprintf("@%s", th.BasicUser2.Username),
}, channelPrivate, false, true)
require.Nil(t, appErr)
// get top threads for team, as user 1 and user 2
// user 1 should see both threads, while user 2 should see only thread in public channel.
topTeamThreadsByUser1, appErr := th.App.GetTopThreadsForTeamSince(th.Context, th.BasicTeam.Id, th.BasicUser.Id, &model.InsightsOpts{StartUnixMilli: 200, PerPage: 100})
require.Nil(t, appErr)
require.Len(t, topTeamThreadsByUser1.Items, 2)
require.Equal(t, topTeamThreadsByUser1.Items[0].Post.Id, rootPostPrivateChannel.Id)
require.Equal(t, topTeamThreadsByUser1.Items[1].Post.Id, rootPostPublicChannel.Id)
topTeamThreadsByUser2, appErr := th.App.GetTopThreadsForTeamSince(th.Context, th.BasicTeam.Id, th.BasicUser2.Id, &model.InsightsOpts{StartUnixMilli: 200, PerPage: 100})
require.Nil(t, appErr)
require.Len(t, topTeamThreadsByUser2.Items, 1)
require.Equal(t, topTeamThreadsByUser2.Items[0].Post.Id, rootPostPublicChannel.Id)
// add user2 to private channel and it can see 2 top threads.
th.AddUserToChannel(th.BasicUser2, channelPrivate)
topTeamThreadsByUser2IncludingPrivate, appErr := th.App.GetTopThreadsForTeamSince(th.Context, th.BasicTeam.Id, th.BasicUser2.Id, &model.InsightsOpts{StartUnixMilli: 200, PerPage: 100})
require.Nil(t, appErr)
require.Len(t, topTeamThreadsByUser2IncludingPrivate.Items, 2)
}
func TestGetTopThreadsForUserSince(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.ConfigStore.SetReadOnlyFF(false)
defer th.ConfigStore.SetReadOnlyFF(true)
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.InsightsEnabled = true })
// create a public channel, a private channel
channelPublic := th.CreateChannel(th.Context, th.BasicTeam)
channelPrivate := th.CreatePrivateChannel(th.Context, th.BasicTeam)
th.AddUserToChannel(th.BasicUser, channelPublic)
th.AddUserToChannel(th.BasicUser, channelPrivate)
th.AddUserToChannel(th.BasicUser2, channelPublic)
th.AddUserToChannel(th.BasicUser2, channelPrivate)
// create two threads: one in public channel, one in private
// post in public channel has both users interacting, post in private only has user1 interacting
rootPostPublicChannel, appErr := th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: channelPublic.Id,
Message: "root post pub",
}, channelPublic, false, true)
require.Nil(t, appErr)
_, appErr = th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser2.Id,
ChannelId: channelPublic.Id,
RootId: rootPostPublicChannel.Id,
Message: "reply post 1",
}, channelPublic, false, true)
require.Nil(t, appErr)
rootPostPrivateChannel, appErr := th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: channelPrivate.Id,
Message: "root post priv",
}, channelPrivate, false, true)
require.Nil(t, appErr)
_, appErr = th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: channelPrivate.Id,
RootId: rootPostPrivateChannel.Id,
Message: "reply post 1",
}, channelPrivate, false, true)
require.Nil(t, appErr)
_, appErr = th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: channelPrivate.Id,
RootId: rootPostPrivateChannel.Id,
Message: "reply post 2",
}, channelPrivate, false, true)
require.Nil(t, appErr)
// get top threads for user, as user 1 and user 2
// user 1 should see both threads, while user 2 should see only thread in public channel
// (even if user2 is in the private channel it hasn't interacted with the thread there.)
topUser1Threads, appErr := th.App.GetTopThreadsForUserSince(th.Context, th.BasicTeam.Id, th.BasicUser.Id, &model.InsightsOpts{StartUnixMilli: 200, PerPage: 100})
require.Nil(t, appErr)
require.Len(t, topUser1Threads.Items, 2)
require.Equal(t, topUser1Threads.Items[0].Post.Id, rootPostPrivateChannel.Id)
require.Equal(t, topUser1Threads.Items[0].ReplyCount, int64(2))
require.Equal(t, topUser1Threads.Items[1].Post.Id, rootPostPublicChannel.Id)
require.Contains(t, topUser1Threads.Items[1].Participants, th.BasicUser2.Id)
require.Equal(t, topUser1Threads.Items[1].ReplyCount, int64(1))
topUser2Threads, appErr := th.App.GetTopThreadsForUserSince(th.Context, th.BasicTeam.Id, th.BasicUser2.Id, &model.InsightsOpts{StartUnixMilli: 200, PerPage: 100})
require.Nil(t, appErr)
require.Len(t, topUser2Threads.Items, 1)
require.Equal(t, topUser2Threads.Items[0].Post.Id, rootPostPublicChannel.Id)
require.Equal(t, topUser2Threads.Items[0].ReplyCount, int64(1))
// deleting the root post results in the thread not making it to top threads list
_, appErr = th.App.DeletePost(th.Context, rootPostPublicChannel.Id, th.BasicUser.Id)
require.Nil(t, appErr)
topUser1ThreadsAfterPost1Delete, appErr := th.App.GetTopThreadsForUserSince(th.Context, th.BasicTeam.Id, th.BasicUser.Id, &model.InsightsOpts{StartUnixMilli: 200, PerPage: 100})
require.Nil(t, appErr)
require.Len(t, topUser1ThreadsAfterPost1Delete.Items, 1)
// reply with user2 in thread2. deleting that reply, shouldn't give any top thread for user2 if the user2 unsubscribes to the thread after deleting the comment
replyPostUser2InPrivate, appErr := th.App.CreatePost(th.Context, &model.Post{
UserId: th.BasicUser2.Id,
ChannelId: channelPrivate.Id,
RootId: rootPostPrivateChannel.Id,
Message: "reply post 3",
}, channelPrivate, false, true)
require.Nil(t, appErr)
topUser2ThreadsAfterPrivateReply, appErr := th.App.GetTopThreadsForUserSince(th.Context, th.BasicTeam.Id, th.BasicUser2.Id, &model.InsightsOpts{StartUnixMilli: 200, PerPage: 100})
require.Nil(t, appErr)
require.Len(t, topUser2ThreadsAfterPrivateReply.Items, 1)
// deleting reply, and unfollowing thread
_, appErr = th.App.DeletePost(th.Context, replyPostUser2InPrivate.Id, th.BasicUser2.Id)
require.Nil(t, appErr)
// unfollow thread
_, err := th.App.Srv().Store().Thread().MaintainMembership(th.BasicUser2.Id, rootPostPrivateChannel.Id, store.ThreadMembershipOpts{
Following: false,
UpdateFollowing: true,
})
require.NoError(t, err)
topUser2ThreadsAfterPrivateReplyDelete, appErr := th.App.GetTopThreadsForUserSince(th.Context, th.BasicTeam.Id, th.BasicUser2.Id, &model.InsightsOpts{StartUnixMilli: 200, PerPage: 100})
require.Nil(t, appErr)
require.Len(t, topUser2ThreadsAfterPrivateReplyDelete.Items, 0)
}
func TestGetEditHistoryForPost(t *testing.T) {
t.Skip("This needs fixing, OriginalId seems to be empty for all posts")
th := Setup(t).InitBasic()
@ -3262,120 +3073,3 @@ func TestGetEditHistoryForPost(t *testing.T) {
require.Empty(t, edits)
})
}
func TestGetTopDMsForUserSince(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.ConfigStore.SetReadOnlyFF(false)
defer th.ConfigStore.SetReadOnlyFF(true)
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.InsightsEnabled = true })
// users
user := th.CreateUser()
u1 := th.CreateUser()
u2 := th.CreateUser()
u3 := th.CreateUser()
u4 := th.CreateUser()
// user direct messages
chUser1, nErr := th.App.createDirectChannel(th.Context, u1.Id, user.Id)
fmt.Println(chUser1, nErr)
require.Nil(t, nErr)
chUser2, nErr := th.App.createDirectChannel(th.Context, u2.Id, user.Id)
require.Nil(t, nErr)
chUser3, nErr := th.App.createDirectChannel(th.Context, u3.Id, user.Id)
require.Nil(t, nErr)
// other user direct message
chUser3User4, nErr := th.App.createDirectChannel(th.Context, u3.Id, u4.Id)
require.Nil(t, nErr)
// sample post data
// for u1
_, err := th.App.CreatePostAsUser(th.Context, &model.Post{
ChannelId: chUser1.Id,
UserId: u1.Id,
}, "", false)
require.Nil(t, err)
_, err = th.App.CreatePostAsUser(th.Context, &model.Post{
ChannelId: chUser1.Id,
UserId: user.Id,
}, "", false)
require.Nil(t, err)
// for u2: 1 post
_, err = th.App.CreatePostAsUser(th.Context, &model.Post{
ChannelId: chUser2.Id,
UserId: u2.Id,
}, "", false)
require.Nil(t, err)
// for user-u3: 3 posts
for i := 0; i < 3; i++ {
_, err = th.App.CreatePostAsUser(th.Context, &model.Post{
ChannelId: chUser3.Id,
UserId: user.Id,
}, "", false)
require.Nil(t, err)
}
// for u4-u3: 4 posts
_, err = th.App.CreatePostAsUser(th.Context, &model.Post{
ChannelId: chUser3User4.Id,
UserId: u3.Id,
}, "", false)
require.Nil(t, err)
_, err = th.App.CreatePostAsUser(th.Context, &model.Post{
ChannelId: chUser3User4.Id,
UserId: u4.Id,
}, "", false)
require.Nil(t, err)
_, err = th.App.CreatePostAsUser(th.Context, &model.Post{
ChannelId: chUser3User4.Id,
UserId: u3.Id,
}, "", false)
require.Nil(t, err)
_, err = th.App.CreatePostAsUser(th.Context, &model.Post{
ChannelId: chUser3User4.Id,
UserId: u4.Id,
}, "", false)
require.Nil(t, err)
t.Run("should return topDMs when userid is specified ", func(t *testing.T) {
topDMs, err := th.App.GetTopDMsForUserSince(user.Id, &model.InsightsOpts{StartUnixMilli: 100, Page: 0, PerPage: 100})
require.Nil(t, err)
// len of topDMs.Items should be 3
require.Len(t, topDMs.Items, 3)
// check order, magnitude of items
// fmt.Println(topDMs.Items[0].MessageCount, topDMs.Items[1].MessageCount, topDMs.Items[2].MessageCount)
require.Equal(t, topDMs.Items[0].SecondParticipant.Id, u3.Id)
require.Equal(t, topDMs.Items[0].MessageCount, int64(3))
require.Equal(t, topDMs.Items[1].SecondParticipant.Id, u1.Id)
require.Equal(t, topDMs.Items[1].MessageCount, int64(2))
require.Equal(t, topDMs.Items[2].SecondParticipant.Id, u2.Id)
require.Equal(t, topDMs.Items[2].MessageCount, int64(1))
// this also ensures that u3-u4 conversation doesn't show up in others' top DMs.
})
t.Run("topDMs should only consider user's DM channels ", func(t *testing.T) {
// u4 only takes part in one conversation
topDMs, err := th.App.GetTopDMsForUserSince(u4.Id, &model.InsightsOpts{StartUnixMilli: 100, Page: 0, PerPage: 100})
require.Nil(t, err)
// len of topDMs.Items should be 3
require.Len(t, topDMs.Items, 1)
// check order, magnitude of items
require.Equal(t, topDMs.Items[0].SecondParticipant.Id, u3.Id)
require.Equal(t, topDMs.Items[0].MessageCount, int64(4))
})
t.Run("topDMs will not consider deleted second user", func(t *testing.T) {
// u4 only takes part in one conversation
topDMs, err := th.App.GetTopDMsForUserSince(u4.Id, &model.InsightsOpts{StartUnixMilli: 100, Page: 0, PerPage: 100})
require.Nil(t, err)
// len of topDMs.Items should be 1
require.Len(t, topDMs.Items, 1)
// delete user3
err = th.App.PermanentDeleteUser(th.Context, u3)
require.Nil(t, err)
topDMs, err = th.App.GetTopDMsForUserSince(u4.Id, &model.InsightsOpts{StartUnixMilli: 100, Page: 0, PerPage: 100})
require.Nil(t, err)
// len of topDMs.Items should be 0 since u3 is deleted
require.Len(t, topDMs.Items, 0)
})
}

View File

@ -100,30 +100,6 @@ func populateEmptyReactions(postIDs []string, reactions map[string][]*model.Reac
return reactions
}
func (a *App) GetTopReactionsForTeamSince(teamID string, userID string, opts *model.InsightsOpts) (*model.TopReactionList, *model.AppError) {
if !a.Config().FeatureFlags.InsightsEnabled {
return nil, model.NewAppError("GetTopReactionsForTeamSince", "api.insights.feature_disabled", nil, "", http.StatusNotImplemented)
}
topReactionList, err := a.Srv().Store().Reaction().GetTopForTeamSince(teamID, userID, opts.StartUnixMilli, opts.Page*opts.PerPage, opts.PerPage)
if err != nil {
return nil, model.NewAppError("GetTopReactionsForTeamSince", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
}
return topReactionList, nil
}
func (a *App) GetTopReactionsForUserSince(userID string, teamID string, opts *model.InsightsOpts) (*model.TopReactionList, *model.AppError) {
if !a.Config().FeatureFlags.InsightsEnabled {
return nil, model.NewAppError("GetTopReactionsForUserSince", "api.insights.feature_disabled", nil, "", http.StatusNotImplemented)
}
topReactionList, err := a.Srv().Store().Reaction().GetTopForUserSince(userID, teamID, opts.StartUnixMilli, opts.Page*opts.PerPage, opts.PerPage)
if err != nil {
return nil, model.NewAppError("GetTopReactionsForUserSince", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
}
return topReactionList, nil
}
func (a *App) DeleteReactionForPost(c *request.Context, reaction *model.Reaction) *model.AppError {
post, err := a.GetSinglePost(reaction.PostId, false)
if err != nil {

View File

@ -5,7 +5,6 @@ package app
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -85,347 +84,3 @@ func TestSharedChannelSyncForReactionActions(t *testing.T) {
assert.Equal(t, channel.Id, sharedChannelService.channelNotifications[1])
})
}
func TestGetTopReactionsForTeamSince(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.Server.platform.SetConfigReadOnlyFF(false)
defer th.Server.platform.SetConfigReadOnlyFF(true)
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.InsightsEnabled = true })
userId := th.BasicUser.Id
user2Id := th.BasicUser2.Id
post1 := th.CreatePost(th.BasicChannel)
post2 := th.CreatePost(th.BasicChannel)
post3 := th.CreatePost(th.BasicChannel)
post4 := th.CreatePost(th.BasicChannel)
post5 := th.CreatePost(th.BasicChannel)
userReactions := []*model.Reaction{
{
UserId: userId,
PostId: post1.Id,
EmojiName: "happy",
},
{
UserId: user2Id,
PostId: post1.Id,
EmojiName: "happy",
},
{
UserId: userId,
PostId: post1.Id,
EmojiName: "sad",
},
{
UserId: user2Id,
PostId: post1.Id,
EmojiName: "sad",
},
{
UserId: userId,
PostId: post1.Id,
EmojiName: "smile",
},
{
UserId: userId,
PostId: post1.Id,
EmojiName: "joy",
},
{
UserId: userId,
PostId: post1.Id,
EmojiName: "100",
},
{
UserId: userId,
PostId: post2.Id,
EmojiName: "sad",
},
{
UserId: userId,
PostId: post2.Id,
EmojiName: "smile",
},
{
UserId: userId,
PostId: post2.Id,
EmojiName: "joy",
},
{
UserId: userId,
PostId: post2.Id,
EmojiName: "100",
},
{
UserId: userId,
PostId: post3.Id,
EmojiName: "smile",
},
{
UserId: user2Id,
PostId: post3.Id,
EmojiName: "smile",
},
{
UserId: userId,
PostId: post3.Id,
EmojiName: "joy",
},
{
UserId: userId,
PostId: post3.Id,
EmojiName: "100",
},
{
UserId: userId,
PostId: post4.Id,
EmojiName: "joy",
},
{
UserId: user2Id,
PostId: post4.Id,
EmojiName: "joy",
},
{
UserId: userId,
PostId: post4.Id,
EmojiName: "100",
},
{
UserId: userId,
PostId: post5.Id,
EmojiName: "100",
},
{
UserId: user2Id,
PostId: post5.Id,
EmojiName: "100",
},
{
UserId: user2Id,
PostId: post5.Id,
EmojiName: "+1",
},
{
UserId: userId,
PostId: post1.Id,
EmojiName: "100",
CreateAt: model.GetMillisForTime(time.Now().Add(time.Hour * time.Duration(-25))),
},
}
for _, userReaction := range userReactions {
_, err := th.App.Srv().Store().Reaction().Save(userReaction)
require.NoError(t, err)
}
teamId := th.BasicChannel.TeamId
var expectedTopReactions [5]*model.TopReaction
expectedTopReactions[0] = &model.TopReaction{EmojiName: "100", Count: int64(6)}
expectedTopReactions[1] = &model.TopReaction{EmojiName: "joy", Count: int64(5)}
expectedTopReactions[2] = &model.TopReaction{EmojiName: "smile", Count: int64(4)}
expectedTopReactions[3] = &model.TopReaction{EmojiName: "sad", Count: int64(3)}
expectedTopReactions[4] = &model.TopReaction{EmojiName: "happy", Count: int64(2)}
timeRange, _ := model.GetStartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location())
t.Run("get-top-reactions-for-team-since", func(t *testing.T) {
topReactions, err := th.App.GetTopReactionsForTeamSince(teamId, userId, &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 0, PerPage: 5})
require.Nil(t, err)
reactions := topReactions.Items
for i, reaction := range reactions {
assert.Equal(t, expectedTopReactions[i].EmojiName, reaction.EmojiName)
assert.Equal(t, expectedTopReactions[i].Count, reaction.Count)
}
topReactions, err = th.App.GetTopReactionsForTeamSince(teamId, userId, &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 1, PerPage: 5})
require.Nil(t, err)
reactions = topReactions.Items
assert.Equal(t, "+1", reactions[0].EmojiName)
assert.Equal(t, int64(1), reactions[0].Count)
})
t.Run("get-top-reactions-for-team-since feature flag", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.InsightsEnabled = false })
defer th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.InsightsEnabled = true })
_, err := th.App.GetTopReactionsForTeamSince(userId, teamId, &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 0, PerPage: 5})
assert.NotNil(t, err)
})
}
func TestGetTopReactionsForUserSince(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.Server.platform.SetConfigReadOnlyFF(false)
defer th.Server.platform.SetConfigReadOnlyFF(true)
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.InsightsEnabled = true })
userId := th.BasicUser.Id
post1 := th.CreatePost(th.BasicChannel)
post2 := th.CreatePost(th.BasicChannel)
post3 := th.CreatePost(th.BasicChannel)
post4 := th.CreatePost(th.BasicChannel)
post5 := th.CreatePost(th.BasicChannel)
post6 := th.CreatePost(th.BasicChannel)
userReactions := []*model.Reaction{
{
UserId: userId,
PostId: post1.Id,
EmojiName: "happy",
},
{
UserId: userId,
PostId: post2.Id,
EmojiName: "happy",
},
{
UserId: userId,
PostId: post3.Id,
EmojiName: "happy",
},
{
UserId: userId,
PostId: post4.Id,
EmojiName: "happy",
},
{
UserId: userId,
PostId: post5.Id,
EmojiName: "happy",
},
{
UserId: userId,
PostId: post6.Id,
EmojiName: "happy",
},
{
UserId: userId,
PostId: post1.Id,
EmojiName: "smile",
},
{
UserId: userId,
PostId: post2.Id,
EmojiName: "smile",
},
{
UserId: userId,
PostId: post3.Id,
EmojiName: "smile",
},
{
UserId: userId,
PostId: post4.Id,
EmojiName: "smile",
},
{
UserId: userId,
PostId: post5.Id,
EmojiName: "smile",
},
{
UserId: userId,
PostId: post1.Id,
EmojiName: "+1",
},
{
UserId: userId,
PostId: post2.Id,
EmojiName: "+1",
},
{
UserId: userId,
PostId: post3.Id,
EmojiName: "+1",
},
{
UserId: userId,
PostId: post4.Id,
EmojiName: "+1",
},
{
UserId: userId,
PostId: post1.Id,
EmojiName: "heart",
},
{
UserId: userId,
PostId: post2.Id,
EmojiName: "heart",
},
{
UserId: userId,
PostId: post3.Id,
EmojiName: "heart",
},
{
UserId: userId,
PostId: post1.Id,
EmojiName: "blush",
},
{
UserId: userId,
PostId: post2.Id,
EmojiName: "blush",
},
{
UserId: userId,
PostId: post1.Id,
EmojiName: "100",
},
{
UserId: userId,
PostId: post1.Id,
EmojiName: "100",
CreateAt: model.GetMillisForTime(time.Now().Add(time.Hour * time.Duration(-25))),
},
}
for _, userReaction := range userReactions {
_, err := th.App.Srv().Store().Reaction().Save(userReaction)
require.NoError(t, err)
}
teamId := th.BasicChannel.TeamId
var expectedTopReactions [5]*model.TopReaction
expectedTopReactions[0] = &model.TopReaction{EmojiName: "happy", Count: int64(6)}
expectedTopReactions[1] = &model.TopReaction{EmojiName: "smile", Count: int64(5)}
expectedTopReactions[2] = &model.TopReaction{EmojiName: "+1", Count: int64(4)}
expectedTopReactions[3] = &model.TopReaction{EmojiName: "heart", Count: int64(3)}
expectedTopReactions[4] = &model.TopReaction{EmojiName: "blush", Count: int64(2)}
timeRange, _ := model.GetStartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location())
t.Run("get-top-reactions-for-user-since", func(t *testing.T) {
topReactions, err := th.App.GetTopReactionsForUserSince(userId, teamId, &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 0, PerPage: 5})
require.Nil(t, err)
reactions := topReactions.Items
for i, reaction := range reactions {
assert.Equal(t, expectedTopReactions[i].EmojiName, reaction.EmojiName)
assert.Equal(t, expectedTopReactions[i].Count, reaction.Count)
}
topReactions, err = th.App.GetTopReactionsForUserSince(userId, teamId, &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 1, PerPage: 5})
require.Nil(t, err)
reactions = topReactions.Items
assert.Equal(t, "100", reactions[0].EmojiName)
assert.Equal(t, int64(1), reactions[0].Count)
})
t.Run("get-top-reactions-for-user-since feature flag", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.InsightsEnabled = false })
defer th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.InsightsEnabled = true })
_, err := th.App.GetTopReactionsForUserSince(userId, teamId, &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 0, PerPage: 5})
assert.NotNil(t, err)
})
}

View File

@ -2116,17 +2116,3 @@ func (a *App) ClearTeamMembersCache(teamID string) error {
}
return nil
}
func (a *App) GetNewTeamMembersSince(c request.CTX, teamID string, opts *model.InsightsOpts) (*model.NewTeamMembersList, int64, *model.AppError) {
if !a.Config().FeatureFlags.InsightsEnabled {
return nil, 0, model.NewAppError("GetNewTeamMembersSince", "app.insights.feature_disabled", nil, "", http.StatusNotImplemented)
}
showFullName := *a.Config().PrivacySettings.ShowFullName || a.SessionHasPermissionTo(*c.Session(), model.PermissionManageSystem)
ntms, count, err := a.Srv().Store().Team().GetNewTeamMembersSince(teamID, opts.StartUnixMilli, opts.Page*opts.PerPage, opts.PerPage, showFullName)
if err != nil {
return nil, 0, model.NewAppError("GetNewTeamMembersSince", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
}
return ntms, count, nil
}

View File

@ -1620,200 +1620,6 @@ func TestInviteGuestsToChannelsGracefully(t *testing.T) {
})
}
func TestGetNewTeamMembersSince(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.ConfigStore.SetReadOnlyFF(false)
defer th.ConfigStore.SetReadOnlyFF(true)
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.InsightsEnabled = true })
team := th.CreateTeam()
t.Run("counts team members", func(t *testing.T) {
var originalExpectedCount int64
var newTeamMemberJoinTime int64
var anotherUser *model.User
t.Run("since time 0", func(t *testing.T) {
teamMembers, err := th.App.Srv().Store().Team().GetMembers(team.Id, 0, 1000, nil)
require.NoError(t, err)
originalExpectedCount = int64(len(teamMembers))
_, actualCount, appErr := th.App.GetNewTeamMembersSince(th.Context, team.Id, &model.InsightsOpts{StartUnixMilli: 0, Page: 0, PerPage: 1000})
require.Nil(t, appErr)
require.Equal(t, originalExpectedCount, actualCount)
})
t.Run("after a new team member was added", func(t *testing.T) {
anotherUser = th.CreateUser()
newTeamMember, appErr := th.App.JoinUserToTeam(th.Context, team, anotherUser, "")
newTeamMemberJoinTime = newTeamMember.CreateAt
require.Nil(t, appErr)
_, actualCount, appErr := th.App.GetNewTeamMembersSince(th.Context, team.Id, &model.InsightsOpts{StartUnixMilli: 0, Page: 0, PerPage: 1000})
require.Nil(t, appErr)
require.Equal(t, originalExpectedCount+1, actualCount)
})
t.Run("after a team member was added to a different team, ensuring the wrong team's member count isn't incremented", func(t *testing.T) {
anotherUser2 := th.CreateUser()
anotherTeam := th.CreateTeam()
_, appErr := th.App.JoinUserToTeam(th.Context, anotherTeam, anotherUser2, "")
require.Nil(t, appErr)
_, actualCount, appErr := th.App.GetNewTeamMembersSince(th.Context, team.Id, &model.InsightsOpts{StartUnixMilli: 0, Page: 0, PerPage: 1000})
require.Nil(t, appErr)
require.Equal(t, originalExpectedCount+1, actualCount)
})
t.Run("since a given time", func(t *testing.T) {
_, actualCount, appErr := th.App.GetNewTeamMembersSince(th.Context, team.Id, &model.InsightsOpts{StartUnixMilli: newTeamMemberJoinTime, Page: 0, PerPage: 1000})
require.Nil(t, appErr)
require.Equal(t, int64(1), actualCount)
})
t.Run("after a team member was removed", func(t *testing.T) {
th.RemoveUserFromTeam(anotherUser, team)
_, actualCount, appErr := th.App.GetNewTeamMembersSince(th.Context, team.Id, &model.InsightsOpts{StartUnixMilli: 0, Page: 0, PerPage: 1000})
require.Nil(t, appErr)
require.Equal(t, originalExpectedCount, actualCount)
})
t.Run("after a user was deactivated", func(t *testing.T) {
_, appErr := th.App.JoinUserToTeam(th.Context, team, anotherUser, "")
require.Nil(t, appErr)
_, beforeCount, appErr := th.App.GetNewTeamMembersSince(th.Context, team.Id, &model.InsightsOpts{StartUnixMilli: 0, Page: 0, PerPage: 1000})
require.Nil(t, appErr)
_, appErr = th.App.UpdateActive(th.Context, anotherUser, false)
defer th.App.UpdateActive(th.Context, anotherUser, true)
require.Nil(t, appErr)
_, afterCount, appErr := th.App.GetNewTeamMembersSince(th.Context, team.Id, &model.InsightsOpts{StartUnixMilli: 0, Page: 0, PerPage: 1000})
require.Nil(t, appErr)
require.Equal(t, beforeCount-1, afterCount)
})
t.Run("after a user was permanently deleted", func(t *testing.T) {
_, beforeCount, appErr := th.App.GetNewTeamMembersSince(th.Context, team.Id, &model.InsightsOpts{StartUnixMilli: 0, Page: 0, PerPage: 1000})
require.Nil(t, appErr)
appErr = th.App.PermanentDeleteUser(th.Context, anotherUser)
require.Nil(t, appErr)
_, afterCount, appErr := th.App.GetNewTeamMembersSince(th.Context, team.Id, &model.InsightsOpts{StartUnixMilli: 0, Page: 0, PerPage: 1000})
require.Nil(t, appErr)
require.Equal(t, beforeCount-1, afterCount)
})
t.Run("exclude bots", func(t *testing.T) {
user := th.CreateUser()
_, appErr := th.App.ConvertUserToBot(user)
require.Nil(t, appErr)
_, appErr = th.App.JoinUserToTeam(th.Context, team, user, "")
require.Nil(t, appErr)
_, actualCount, appErr := th.App.GetNewTeamMembersSince(th.Context, team.Id, &model.InsightsOpts{StartUnixMilli: 0, Page: 0, PerPage: 1000})
require.Nil(t, appErr)
require.Equal(t, originalExpectedCount, actualCount)
})
})
t.Run("returns the correct team members", func(t *testing.T) {
var originalExpectedMembers []*model.TeamMember
var newTeamMemberJoinTime int64
var anotherUser *model.User
uIDs := func(members []*model.TeamMember) []string {
ids := []string{}
for _, member := range members {
ids = append(ids, member.UserId)
}
return ids
}
nUIDs := func(members []*model.NewTeamMember) []string {
ids := []string{}
for _, member := range members {
ids = append(ids, member.Id)
}
return ids
}
t.Run("since time 0", func(t *testing.T) {
var err error
originalExpectedMembers, err = th.App.Srv().Store().Team().GetMembers(th.BasicTeam.Id, 0, 1000, nil)
require.NoError(t, err)
actualMembersList, _, appErr := th.App.GetNewTeamMembersSince(th.Context, th.BasicTeam.Id, &model.InsightsOpts{StartUnixMilli: 0, Page: 0, PerPage: 1000})
require.Nil(t, appErr)
require.ElementsMatch(t, uIDs(originalExpectedMembers), nUIDs(actualMembersList.Items))
})
t.Run("after a new team member was added", func(t *testing.T) {
anotherUser = th.CreateUser()
newTeamMember, appErr := th.App.JoinUserToTeam(th.Context, th.BasicTeam, anotherUser, "")
newTeamMemberJoinTime = newTeamMember.CreateAt
require.Nil(t, appErr)
actualMembersList, _, appErr := th.App.GetNewTeamMembersSince(th.Context, th.BasicTeam.Id, &model.InsightsOpts{StartUnixMilli: 0, Page: 0, PerPage: 1000})
require.Nil(t, appErr)
require.ElementsMatch(t, append(uIDs(originalExpectedMembers), anotherUser.Id), nUIDs(actualMembersList.Items))
})
t.Run("after a team member was added to a different team, ensuring the wrong team's member count isn't incremented", func(t *testing.T) {
anotherUser2 := th.CreateUser()
anotherTeam := th.CreateTeam()
_, appErr := th.App.JoinUserToTeam(th.Context, anotherTeam, anotherUser2, "")
require.Nil(t, appErr)
actualMembersList, _, appErr := th.App.GetNewTeamMembersSince(th.Context, th.BasicTeam.Id, &model.InsightsOpts{StartUnixMilli: 0, Page: 0, PerPage: 1000})
require.Nil(t, appErr)
require.ElementsMatch(t, append(uIDs(originalExpectedMembers), anotherUser.Id), nUIDs(actualMembersList.Items))
})
t.Run("since a given time", func(t *testing.T) {
actualMembersList, _, appErr := th.App.GetNewTeamMembersSince(th.Context, th.BasicTeam.Id, &model.InsightsOpts{StartUnixMilli: newTeamMemberJoinTime, Page: 0, PerPage: 1000})
require.Nil(t, appErr)
require.Len(t, actualMembersList.Items, 1)
require.Equal(t, anotherUser.Id, actualMembersList.Items[0].Id)
})
t.Run("after a team member was removed", func(t *testing.T) {
th.RemoveUserFromTeam(anotherUser, th.BasicTeam)
actualMembersList, _, appErr := th.App.GetNewTeamMembersSince(th.Context, th.BasicTeam.Id, &model.InsightsOpts{StartUnixMilli: 0, Page: 0, PerPage: 1000})
require.Nil(t, appErr)
require.ElementsMatch(t, uIDs(originalExpectedMembers), nUIDs(actualMembersList.Items))
})
t.Run("after a user was deactivated", func(t *testing.T) {
_, appErr := th.App.JoinUserToTeam(th.Context, th.BasicTeam, anotherUser, "")
require.Nil(t, appErr)
beforeMembersList, _, appErr := th.App.GetNewTeamMembersSince(th.Context, th.BasicTeam.Id, &model.InsightsOpts{StartUnixMilli: 0, Page: 0, PerPage: 1000})
require.Nil(t, appErr)
require.Contains(t, nUIDs(beforeMembersList.Items), anotherUser.Id)
_, appErr = th.App.UpdateActive(th.Context, anotherUser, false)
defer th.App.UpdateActive(th.Context, anotherUser, true)
require.Nil(t, appErr)
afterMembersList, _, appErr := th.App.GetNewTeamMembersSince(th.Context, th.BasicTeam.Id, &model.InsightsOpts{StartUnixMilli: 0, Page: 0, PerPage: 1000})
require.Nil(t, appErr)
require.NotContains(t, nUIDs(afterMembersList.Items), anotherUser.Id)
})
t.Run("after a user was permanently deleted", func(t *testing.T) {
beforeMembersList, _, appErr := th.App.GetNewTeamMembersSince(th.Context, th.BasicTeam.Id, &model.InsightsOpts{StartUnixMilli: 0, Page: 0, PerPage: 1000})
require.Nil(t, appErr)
require.Contains(t, nUIDs(beforeMembersList.Items), anotherUser.Id)
appErr = th.App.PermanentDeleteUser(th.Context, anotherUser)
require.Nil(t, appErr)
afterMembersList, _, appErr := th.App.GetNewTeamMembersSince(th.Context, th.BasicTeam.Id, &model.InsightsOpts{StartUnixMilli: 0, Page: 0, PerPage: 1000})
require.Nil(t, appErr)
require.NotContains(t, nUIDs(afterMembersList.Items), anotherUser.Id)
})
t.Run("exclude bots", func(t *testing.T) {
user := th.CreateUser()
_, appErr := th.App.ConvertUserToBot(user)
require.Nil(t, appErr)
_, appErr = th.App.JoinUserToTeam(th.Context, th.BasicTeam, user, "")
require.Nil(t, appErr)
actualMembersList, _, appErr := th.App.GetNewTeamMembersSince(th.Context, th.BasicTeam.Id, &model.InsightsOpts{StartUnixMilli: 0, Page: 0, PerPage: 1000})
require.Nil(t, appErr)
require.ElementsMatch(t, uIDs(originalExpectedMembers), nUIDs(actualMembersList.Items))
})
})
}
func TestTeamSendEvents(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()

View File

@ -284,14 +284,6 @@ func (a *App) createUserOrGuest(c request.CTX, user *model.User, guest bool) (*m
tutorialStepPref := model.Preference{UserId: ruser.Id, Category: model.PreferenceCategoryTutorialSteps, Name: ruser.Id, Value: "0"}
preferences := model.Preferences{recommendedNextStepsPref, tutorialStepPref}
if a.Config().FeatureFlags.InsightsEnabled {
// We don't want to show the insights intro modal for new users
preferences = append(preferences, model.Preference{UserId: ruser.Id, Category: model.PreferenceCategoryInsights, Name: model.PreferenceNameInsights, Value: "{\"insights_modal_viewed\":true}"})
} else {
preferences = append(preferences, model.Preference{UserId: ruser.Id, Category: model.PreferenceCategoryInsights, Name: model.PreferenceNameInsights, Value: "{\"insights_modal_viewed\":false}"})
}
if err := a.Srv().Store().Preference().Save(preferences); err != nil {
c.Logger().Warn("Encountered error saving user preferences", mlog.Err(err))
}

View File

@ -1810,16 +1810,10 @@ func TestCreateUserWithInitialPreferences(t *testing.T) {
t.Run("successfully create a user with initial tutorial and recommended steps preferences", func(t *testing.T) {
th.ConfigStore.SetReadOnlyFF(false)
defer th.ConfigStore.SetReadOnlyFF(true)
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.InsightsEnabled = true })
testUser := th.CreateUser()
defer th.App.PermanentDeleteUser(th.Context, testUser)
insightsPref, appErr := th.App.GetPreferenceByCategoryAndNameForUser(testUser.Id, model.PreferenceCategoryInsights, model.PreferenceNameInsights)
require.Nil(t, appErr)
assert.Equal(t, "insights_tutorial_state", insightsPref.Name)
assert.Equal(t, "{\"insights_modal_viewed\":true}", insightsPref.Value)
tutorialStepPref, appErr := th.App.GetPreferenceByCategoryAndNameForUser(testUser.Id, model.PreferenceCategoryTutorialSteps, testUser.Id)
require.Nil(t, appErr)
assert.Equal(t, testUser.Id, tutorialStepPref.Name)
@ -1831,38 +1825,12 @@ func TestCreateUserWithInitialPreferences(t *testing.T) {
assert.Equal(t, "false", recommendedNextStepsPref[0].Value)
})
t.Run("successfully create a user with insights feature flag disabled", func(t *testing.T) {
t.Run("successfully create a guest user with initial tutorial and recommended steps preferences", func(t *testing.T) {
th.Server.platform.SetConfigReadOnlyFF(false)
defer th.Server.platform.SetConfigReadOnlyFF(true)
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.InsightsEnabled = false })
defer th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.InsightsEnabled = true })
testUser := th.CreateUser()
defer th.App.PermanentDeleteUser(th.Context, testUser)
insightsPref, appErr := th.App.GetPreferenceByCategoryAndNameForUser(testUser.Id, model.PreferenceCategoryInsights, model.PreferenceNameInsights)
require.Nil(t, appErr)
assert.Equal(t, "insights_tutorial_state", insightsPref.Name)
assert.Equal(t, "{\"insights_modal_viewed\":false}", insightsPref.Value)
recommendedNextStepsPref, appErr := th.App.GetPreferenceByCategoryForUser(testUser.Id, model.PreferenceRecommendedNextSteps)
require.Nil(t, appErr)
assert.Equal(t, model.PreferenceRecommendedNextSteps, recommendedNextStepsPref[0].Category)
assert.Equal(t, "hide", recommendedNextStepsPref[0].Name)
assert.Equal(t, "false", recommendedNextStepsPref[0].Value)
})
t.Run("successfully create a guest user with initial tutorial, insights and recommended steps preferences", func(t *testing.T) {
th.Server.platform.SetConfigReadOnlyFF(false)
defer th.Server.platform.SetConfigReadOnlyFF(true)
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.InsightsEnabled = true })
testUser := th.CreateGuest()
defer th.App.PermanentDeleteUser(th.Context, testUser)
insightsPref, appErr := th.App.GetPreferenceByCategoryAndNameForUser(testUser.Id, model.PreferenceCategoryInsights, model.PreferenceNameInsights)
require.Nil(t, appErr)
assert.Equal(t, "insights_tutorial_state", insightsPref.Name)
assert.Equal(t, "{\"insights_modal_viewed\":true}", insightsPref.Value)
tutorialStepPref, appErr := th.App.GetPreferenceByCategoryAndNameForUser(testUser.Id, model.PreferenceCategoryTutorialSteps, testUser.Id)
require.Nil(t, appErr)
assert.Equal(t, testUser.Id, tutorialStepPref.Name)

View File

@ -8,7 +8,6 @@ package opentracinglayer
import (
"context"
"time"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/v8/platform/services/tracing"

View File

@ -9,7 +9,6 @@ package retrylayer
import (
"context"
timepkg "time"
"time"
"github.com/lib/pq"
"github.com/mattermost/mattermost/server/public/model"

View File

@ -8,7 +8,6 @@ package opentracinglayer
import (
"context"
"time"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/v8/channels/store"
@ -1876,78 +1875,6 @@ func (s *OpenTracingLayerChannelStore) GetTeamMembersForChannel(channelID string
return result, err
}
func (s *OpenTracingLayerChannelStore) GetTopChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetTopChannelsForTeamSince")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetTopChannelsForTeamSince(teamID, userID, since, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetTopChannelsForUserSince(userID string, teamID string, since int64, offset int, limit int) (*model.TopChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetTopChannelsForUserSince")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetTopChannelsForUserSince(userID, teamID, since, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetTopInactiveChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetTopInactiveChannelsForTeamSince")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetTopInactiveChannelsForTeamSince(teamID, userID, since, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GetTopInactiveChannelsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetTopInactiveChannelsForUserSince")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.GetTopInactiveChannelsForUserSince(teamID, userID, since, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) GroupSyncedChannelCount() (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GroupSyncedChannelCount")
@ -2178,24 +2105,6 @@ func (s *OpenTracingLayerChannelStore) PermanentDeleteMembersByUser(userID strin
return err
}
func (s *OpenTracingLayerChannelStore) PostCountsByDuration(channelIDs []string, sinceUnixMillis int64, userID *string, duration model.PostCountGrouping, groupingLocation *time.Location) ([]*model.DurationPostCount, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.PostCountsByDuration")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ChannelStore.PostCountsByDuration(channelIDs, sinceUnixMillis, userID, duration, groupingLocation)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerChannelStore) RemoveAllDeactivatedMembers(channelID string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.RemoveAllDeactivatedMembers")
@ -6458,24 +6367,6 @@ func (s *OpenTracingLayerPostStore) GetSingle(id string, inclDeleted bool) (*mod
return result, err
}
func (s *OpenTracingLayerPostStore) GetTopDMsForUserSince(userID string, since int64, offset int, limit int) (*model.TopDMList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetTopDMsForUserSince")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostStore.GetTopDMsForUserSince(userID, since, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostStore) HasAutoResponsePostByUserSince(options model.GetPostsSinceOptions, userId string) (bool, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.HasAutoResponsePostByUserSince")
@ -7353,42 +7244,6 @@ func (s *OpenTracingLayerReactionStore) GetForPostSince(postId string, since int
return result, err
}
func (s *OpenTracingLayerReactionStore) GetTopForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopReactionList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ReactionStore.GetTopForTeamSince")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ReactionStore.GetTopForTeamSince(teamID, userID, since, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerReactionStore) GetTopForUserSince(userID string, teamID string, since int64, offset int, limit int) (*model.TopReactionList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ReactionStore.GetTopForUserSince")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ReactionStore.GetTopForUserSince(userID, teamID, since, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerReactionStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ReactionStore.PermanentDeleteBatch")
@ -9634,24 +9489,6 @@ func (s *OpenTracingLayerTeamStore) GetMembersByIds(teamID string, userIds []str
return result, err
}
func (s *OpenTracingLayerTeamStore) GetNewTeamMembersSince(teamID string, since int64, offset int, limit int, showFullName bool) (*model.NewTeamMembersList, int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetNewTeamMembersSince")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, resultVar1, err := s.TeamStore.GetNewTeamMembersSince(teamID, since, offset, limit, showFullName)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, resultVar1, err
}
func (s *OpenTracingLayerTeamStore) GetTeamMembersForExport(userID string) ([]*model.TeamMemberForExport, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "TeamStore.GetTeamMembersForExport")
@ -10421,42 +10258,6 @@ func (s *OpenTracingLayerThreadStore) GetThreadsForUser(userId string, teamID st
return result, err
}
func (s *OpenTracingLayerThreadStore) GetTopThreadsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.GetTopThreadsForTeamSince")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ThreadStore.GetTopThreadsForTeamSince(teamID, userID, since, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerThreadStore) GetTopThreadsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.GetTopThreadsForUserSince")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ThreadStore.GetTopThreadsForUserSince(teamID, userID, since, offset, limit)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerThreadStore) GetTotalThreads(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ThreadStore.GetTotalThreads")

View File

@ -8,7 +8,6 @@ package retrylayer
import (
"context"
"time"
timepkg "time"
"github.com/go-sql-driver/mysql"
@ -2105,90 +2104,6 @@ func (s *RetryLayerChannelStore) GetTeamMembersForChannel(channelID string) ([]s
}
func (s *RetryLayerChannelStore) GetTopChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.GetTopChannelsForTeamSince(teamID, userID, since, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetTopChannelsForUserSince(userID string, teamID string, since int64, offset int, limit int) (*model.TopChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.GetTopChannelsForUserSince(userID, teamID, since, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetTopInactiveChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.GetTopInactiveChannelsForTeamSince(teamID, userID, since, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GetTopInactiveChannelsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error) {
tries := 0
for {
result, err := s.ChannelStore.GetTopInactiveChannelsForUserSince(teamID, userID, since, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) GroupSyncedChannelCount() (int64, error) {
tries := 0
@ -2384,27 +2299,6 @@ func (s *RetryLayerChannelStore) PermanentDeleteMembersByUser(userID string) err
}
func (s *RetryLayerChannelStore) PostCountsByDuration(channelIDs []string, sinceUnixMillis int64, userID *string, duration model.PostCountGrouping, groupingLocation *time.Location) ([]*model.DurationPostCount, error) {
tries := 0
for {
result, err := s.ChannelStore.PostCountsByDuration(channelIDs, sinceUnixMillis, userID, duration, groupingLocation)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerChannelStore) RemoveAllDeactivatedMembers(channelID string) error {
tries := 0
@ -7313,27 +7207,6 @@ func (s *RetryLayerPostStore) GetSingle(id string, inclDeleted bool) (*model.Pos
}
func (s *RetryLayerPostStore) GetTopDMsForUserSince(userID string, since int64, offset int, limit int) (*model.TopDMList, error) {
tries := 0
for {
result, err := s.PostStore.GetTopDMsForUserSince(userID, since, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostStore) HasAutoResponsePostByUserSince(options model.GetPostsSinceOptions, userId string) (bool, error) {
tries := 0
@ -8348,48 +8221,6 @@ func (s *RetryLayerReactionStore) GetForPostSince(postId string, since int64, ex
}
func (s *RetryLayerReactionStore) GetTopForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopReactionList, error) {
tries := 0
for {
result, err := s.ReactionStore.GetTopForTeamSince(teamID, userID, since, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerReactionStore) GetTopForUserSince(userID string, teamID string, since int64, offset int, limit int) (*model.TopReactionList, error) {
tries := 0
for {
result, err := s.ReactionStore.GetTopForUserSince(userID, teamID, since, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerReactionStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
tries := 0
@ -11000,27 +10831,6 @@ func (s *RetryLayerTeamStore) GetMembersByIds(teamID string, userIds []string, r
}
func (s *RetryLayerTeamStore) GetNewTeamMembersSince(teamID string, since int64, offset int, limit int, showFullName bool) (*model.NewTeamMembersList, int64, error) {
tries := 0
for {
result, resultVar1, err := s.TeamStore.GetNewTeamMembersSince(teamID, since, offset, limit, showFullName)
if err == nil {
return result, resultVar1, nil
}
if !isRepeatableError(err) {
return result, resultVar1, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, resultVar1, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerTeamStore) GetTeamMembersForExport(userID string) ([]*model.TeamMemberForExport, error) {
tries := 0
@ -11909,48 +11719,6 @@ func (s *RetryLayerThreadStore) GetThreadsForUser(userId string, teamID string,
}
func (s *RetryLayerThreadStore) GetTopThreadsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
tries := 0
for {
result, err := s.ThreadStore.GetTopThreadsForTeamSince(teamID, userID, since, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) GetTopThreadsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
tries := 0
for {
result, err := s.ThreadStore.GetTopThreadsForUserSince(teamID, userID, since, offset, limit)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerThreadStore) GetTotalThreads(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
tries := 0

View File

@ -4342,414 +4342,3 @@ func (s SqlChannelStore) GetTeamForChannel(channelID string) (*model.Team, error
}
return &team, nil
}
// GetTopChannelsForTeamSince returns the filtered post counts of the following Channels sets:
// a) those that are private channels in the given user's membership graph on the given team, and
// b) those that are public channels in the given team.
func (s SqlChannelStore) GetTopChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopChannelList, error) {
channels := make([]*model.TopChannel, 0)
var args []any
postgresPropQuery := `AND (Posts.Props ->> 'from_bot' IS NULL OR Posts.Props ->> 'from_bot' = 'false') AND (Posts.Props ->> 'from_webhook' IS NULL OR Posts.Props ->> 'from_webhook' = 'false') AND (Posts.Props ->> 'from_oauth_app' IS NULL OR Posts.Props ->> 'from_oauth_app' = 'false') AND (Posts.Props ->> 'from_plugin' IS NULL OR Posts.Props ->> 'from_plugin' = 'false')`
mySqlPropsQuery := `AND (JSON_EXTRACT(Posts.Props, '$.from_bot') IS NULL OR JSON_EXTRACT(Posts.Props, '$.from_bot') = 'false') AND (JSON_EXTRACT(Posts.Props, '$.from_webhook') IS NULL OR JSON_EXTRACT(Posts.Props, '$.from_webhook') = 'false') AND (JSON_EXTRACT(Posts.Props, '$.from_plugin') IS NULL OR JSON_EXTRACT(Posts.Props, '$.from_plugin') = 'false') AND (JSON_EXTRACT(Posts.Props, '$.from_oauth_app') IS NULL OR JSON_EXTRACT(Posts.Props, '$.from_oauth_app') = 'false')`
query := `
SELECT
ID,
Type,
DisplayName,
Name,
TeamID,
MessageCount
FROM
((SELECT
Posts.ChannelId AS ID,
'O' AS Type,
PublicChannels.DisplayName AS DisplayName,
PublicChannels.Name AS Name,
PublicChannels.TeamId AS TeamID,
count(Posts.Id) AS MessageCount,
PublicChannels.DeleteAt AS DeleteAt
FROM
Posts
LEFT JOIN PublicChannels on Posts.ChannelId = PublicChannels.Id
WHERE
Posts.DeleteAt = 0
AND Posts.CreateAt > ?
AND Posts.Type = ''`
args = []any{since}
if s.DriverName() == model.DatabaseDriverMysql {
query += mySqlPropsQuery
} else if s.DriverName() == model.DatabaseDriverPostgres {
query += postgresPropQuery
}
query += `
AND PublicChannels.TeamId = ?
GROUP BY
Posts.ChannelId,
PublicChannels.DisplayName,
PublicChannels.Name,
PublicChannels.TeamId,
PublicChannels.DeleteAt)
UNION ALL
(SELECT
Posts.ChannelId AS ID,
Channels.Type AS Type,
Channels.DisplayName AS DisplayName,
Channels.Name AS Name,
Channels.TeamId AS TeamID,
count(Posts.Id) AS MessageCount,
Channels.DeleteAt AS DeleteAt
FROM
Posts
LEFT JOIN Channels on Posts.ChannelId = Channels.Id
LEFT JOIN ChannelMembers on Posts.ChannelId = ChannelMembers.ChannelId
WHERE
Posts.DeleteAt = 0
AND Posts.CreateAt > ?
AND Posts.Type = ''`
args = append(args, teamID, since)
if s.DriverName() == model.DatabaseDriverMysql {
query += mySqlPropsQuery
} else if s.DriverName() == model.DatabaseDriverPostgres {
query += postgresPropQuery
}
query += `
AND Channels.TeamId = ?
AND Channels.Type = 'P'
AND ChannelMembers.UserId = ?
GROUP BY
Posts.ChannelId,
Channels.Type,
Channels.DisplayName,
Channels.Name,
Channels.TeamId,
Channels.DeleteAt)) AS A
WHERE
DeleteAt = 0
ORDER BY
MessageCount DESC,
Name ASC
LIMIT ?
OFFSET ?`
args = append(args, teamID, userID, limit+1, offset)
if err := s.GetReplicaX().Select(&channels, query, args...); err != nil {
return nil, errors.Wrap(err, "failed to get top Channels")
}
return model.GetTopChannelListWithPagination(channels, limit), nil
}
// GetTopChannelsForUserSince returns the filtered post counts of channels with with posts created by the user
// after the given timestamp within the given team (or across the workspace if no team is given). Excludes DM and GM channels.
func (s SqlChannelStore) GetTopChannelsForUserSince(userID string, teamID string, since int64, offset int, limit int) (*model.TopChannelList, error) {
channels := make([]*model.TopChannel, 0)
var args []any
var query string
var propsQuery string
if s.DriverName() == model.DatabaseDriverMysql {
propsQuery = `AND (JSON_EXTRACT(Posts.Props, '$.from_bot') IS NULL OR JSON_EXTRACT(Posts.Props, '$.from_bot') = 'false') AND (JSON_EXTRACT(Posts.Props, '$.from_webhook') IS NULL OR JSON_EXTRACT(Posts.Props, '$.from_webhook') = 'false') AND (JSON_EXTRACT(Posts.Props, '$.from_plugin') IS NULL OR JSON_EXTRACT(Posts.Props, '$.from_plugin') = 'false') AND (JSON_EXTRACT(Posts.Props, '$.from_oauth_app') IS NULL OR JSON_EXTRACT(Posts.Props, '$.from_oauth_app') = 'false')`
} else if s.DriverName() == model.DatabaseDriverPostgres {
propsQuery = `AND (Posts.Props ->> 'from_bot' IS NULL OR Posts.Props ->> 'from_bot' = 'false') AND (Posts.Props ->> 'from_webhook' IS NULL OR Posts.Props ->> 'from_webhook' = 'false') AND (Posts.Props ->> 'from_oauth_app' IS NULL OR Posts.Props ->> 'from_oauth_app' = 'false') AND (Posts.Props ->> 'from_plugin' IS NULL OR Posts.Props ->> 'from_plugin' = 'false')`
}
query = `
SELECT
Posts.ChannelId AS ID,
Channels.Type AS Type,
Channels.DisplayName AS DisplayName,
Channels.Name AS Name,
Channels.TeamId AS TeamID,
count(Posts.Id) AS MessageCount
FROM
Posts
LEFT JOIN Channels on Posts.ChannelId = Channels.Id
LEFT JOIN ChannelMembers on Posts.ChannelId = ChannelMembers.ChannelId
WHERE
Posts.DeleteAt = 0
AND Posts.CreateAt > ?
AND Posts.Type = ''
AND Posts.UserID = ?
AND Channels.DeleteAt = 0
AND (Channels.Type = 'O' OR Channels.Type = 'P')
AND ChannelMembers.UserId = ? `
query += propsQuery
args = []any{since, userID, userID}
if teamID != "" {
query += `
AND Channels.TeamID = ?`
args = append(args, teamID)
}
query += `
Group By
Posts.ChannelId,
Channels.Type,
Channels.DisplayName,
Channels.Name,
Channels.TeamId
ORDER BY
MessageCount DESC,
Name ASC
LIMIT ?
OFFSET ?`
args = append(args, limit+1, offset)
if err := s.GetReplicaX().Select(&channels, query, args...); err != nil {
return nil, errors.Wrap(err, "failed to get top Channels")
}
return model.GetTopChannelListWithPagination(channels, limit), nil
}
// GetTopInactiveChannelsForTeamSince returns the filtered post counts of the following Channels sets:
// a) those that are private channels in the given user's membership graph on the given team, and
// b) those that are public channels in the given team.
func (s SqlChannelStore) GetTopInactiveChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error) {
channels := make([]*model.TopInactiveChannel, 0)
var args []any
query := `
SELECT
ID,
Type,
DisplayName,
Name,
MessageCount,
LastActivityAt
FROM
((SELECT
PublicChannels.Id AS ID,
'O' AS Type,
PublicChannels.DisplayName AS DisplayName,
PublicChannels.Name AS Name,
COALESCE(count(Posts.Id), 0) AS MessageCount,
COALESCE(max(Posts.CreateAt), 0) AS LastActivityAt
FROM
PublicChannels
LEFT JOIN Posts on Posts.ChannelId = PublicChannels.Id AND Posts.Type = '' AND Posts.CreateAt > ? AND Posts.DeleteAt = 0
LEFT JOIN Channels on Channels.Id = PublicChannels.Id
WHERE
PublicChannels.TeamId = ?
AND PublicChannels.DeleteAt = 0
AND Channels.CreateAt < ?
GROUP BY
PublicChannels.Id,
PublicChannels.DisplayName,
PublicChannels.Name,
PublicChannels.TeamId)
UNION ALL
(SELECT
Channels.Id AS ID,
Channels.Type AS Type,
Channels.DisplayName AS DisplayName,
Channels.Name AS Name,
COALESCE(count(Posts.Id), 0) AS MessageCount,
COALESCE(max(Posts.CreateAt), 0) AS LastActivityAt
FROM
Channels
LEFT JOIN Posts on Posts.ChannelId = Channels.Id AND Posts.Type = '' AND Posts.CreateAt > ? AND Posts.DeleteAt = 0
LEFT JOIN ChannelMembers on Channels.Id = ChannelMembers.ChannelId
WHERE
Channels.TeamId = ?
AND Channels.CreateAt < ?
AND Channels.Type = 'P'
AND Channels.DeleteAt = 0
AND ChannelMembers.UserId = ?
GROUP BY
Channels.Id,
Channels.Type,
Channels.DisplayName,
Channels.Name)) AS A
ORDER BY
MessageCount ASC,
Name ASC
LIMIT ?
OFFSET ?`
args = append(args, since, teamID, since, since, teamID, since, userID, limit+1, offset)
if err := s.GetReplicaX().Select(&channels, query, args...); err != nil {
return nil, errors.Wrap(err, "failed to get top Channels")
}
channels, err := postProcessTopInactiveChannels(s, channels)
if err != nil {
return nil, err
}
return model.GetTopInactiveChannelListWithPagination(channels, limit), nil
}
// GetTopInactiveChannelsForUserSince returns the filtered post counts of channels with with posts created by the user
// after the given timestamp within the given team (or across the workspace if no team is given). Excludes DM and GM channels.
func (s SqlChannelStore) GetTopInactiveChannelsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error) {
channels := make([]*model.TopInactiveChannel, 0)
var args []any
var query string
query = `
SELECT
Channels.Id AS ID,
Channels.Type AS Type,
Channels.DisplayName AS DisplayName,
Channels.Name AS Name,
COALESCE(count(Posts.Id), 0) AS MessageCount,
COALESCE(max(Posts.CreateAt), 0) AS LastActivityAt
FROM
Channels
LEFT JOIN Posts on Posts.ChannelId = Channels.Id AND Posts.Type = '' AND Posts.CreateAt > ? AND Posts.DeleteAt = 0
LEFT JOIN ChannelMembers on Channels.Id = ChannelMembers.ChannelId
WHERE
Channels.DeleteAt = 0
AND Channels.CreateAt < ?
AND (Channels.Type = 'O' OR Channels.Type = 'P')
AND ChannelMembers.UserId = ? `
args = []any{since, since, userID}
if teamID != "" {
query += `
AND Channels.TeamID = ?`
args = append(args, teamID)
}
query += `
Group By
Channels.Id,
Channels.Type,
Channels.DisplayName,
Channels.Name
ORDER BY
MessageCount ASC,
Name ASC
LIMIT ?
OFFSET ?`
args = append(args, limit+1, offset)
if err := s.GetReplicaX().Select(&channels, query, args...); err != nil {
return nil, errors.Wrap(err, "failed to get top Inactive Channels")
}
channels, err := postProcessTopInactiveChannels(s, channels)
if err != nil {
return nil, err
}
return model.GetTopInactiveChannelListWithPagination(channels, limit), nil
}
func postProcessTopInactiveChannels(s SqlChannelStore, channels []*model.TopInactiveChannel) ([]*model.TopInactiveChannel, error) {
// query channel members for Ids
var conditionalAggrSelector string
if s.DriverName() == model.DatabaseDriverMysql {
conditionalAggrSelector = "GROUP_CONCAT(UserId SEPARATOR ',') as UserIds"
} else if s.DriverName() == model.DatabaseDriverPostgres {
conditionalAggrSelector = "string_agg(UserId, ',') as UserIds"
}
var channelIds []string
for _, channel := range channels {
channelIds = append(channelIds, channel.ID)
}
q := s.getQueryBuilder().Select("ChannelId", conditionalAggrSelector).From("ChannelMembers").
Where(sq.Eq{
"ChannelId": channelIds,
}).GroupBy("ChannelId")
channelsUserIdsMap := make(map[string]string, len(channels))
type ChannelUserIdsResult struct {
ChannelId string
UserIds string
}
channelsUserIdsResultList := make([]ChannelUserIdsResult, len(channels))
sql, args, err := q.ToSql()
if err != nil {
return nil, errors.Wrap(err, "failed to stringify squirrel query")
}
if err := s.GetReplicaX().Select(&channelsUserIdsResultList, sql, args...); err != nil {
return nil, errors.Wrap(err, "failed to get top Inactive Channels users")
}
for _, channelUserIds := range channelsUserIdsResultList {
channelsUserIdsMap[channelUserIds.ChannelId] = channelUserIds.UserIds
}
for index, channel := range channels {
userIds := channelsUserIdsMap[channel.ID]
userIdsSlice := strings.Split(userIds, ",")
channels[index].Participants = userIdsSlice
// handle channels with 0 participants
if len(userIdsSlice) == 1 && userIdsSlice[0] == "" {
channels[index].Participants = make([]string, 0)
}
}
return channels, nil
}
func (s SqlChannelStore) PostCountsByDuration(channelIDs []string, sinceUnixMillis int64, userID *string, duration model.PostCountGrouping, atLocation *time.Location) ([]*model.DurationPostCount, error) {
var unixSelect string
var propsQuery string
loc := atLocation.String()
if loc == "Local" {
loc = "UTC"
}
var format string
if s.DriverName() == model.DatabaseDriverMysql {
if duration == model.PostsByDay {
format = `%Y-%m-%d`
} else {
format = `%Y-%m-%dT%H`
}
unixSelect = fmt.Sprintf(`DATE_FORMAT(
COALESCE(
CONVERT_TZ(FROM_UNIXTIME(Posts.CreateAt / 1000), 'GMT', '%s'),
FROM_UNIXTIME(Posts.CreateAt / 1000)
),
'%s') AS duration`, loc, format)
propsQuery = `(JSON_EXTRACT(Posts.Props, '$.from_bot') IS NULL OR JSON_EXTRACT(Posts.Props, '$.from_bot') = 'false') AND (JSON_EXTRACT(Posts.Props, '$.from_webhook') IS NULL OR JSON_EXTRACT(Posts.Props, '$.from_webhook') = 'false') AND (JSON_EXTRACT(Posts.Props, '$.from_plugin') IS NULL OR JSON_EXTRACT(Posts.Props, '$.from_plugin') = 'false') AND (JSON_EXTRACT(Posts.Props, '$.from_oauth_app') IS NULL OR JSON_EXTRACT(Posts.Props, '$.from_oauth_app') = 'false')`
} else if s.DriverName() == model.DatabaseDriverPostgres {
if duration == model.PostsByDay {
format = "YYYY-MM-DD"
} else {
format = `YYYY-MM-DD"T"HH24`
}
unixSelect = fmt.Sprintf(`TO_CHAR(TO_TIMESTAMP(Posts.CreateAt / 1000) AT TIME ZONE '%s', '%s') AS duration`, loc, format)
propsQuery = `(Posts.Props ->> 'from_bot' IS NULL OR Posts.Props ->> 'from_bot' = 'false') AND (Posts.Props ->> 'from_webhook' IS NULL OR Posts.Props ->> 'from_webhook' = 'false') AND (Posts.Props ->> 'from_oauth_app' IS NULL OR Posts.Props ->> 'from_oauth_app' = 'false') AND (Posts.Props ->> 'from_plugin' IS NULL OR Posts.Props ->> 'from_plugin' = 'false')`
}
query := sq.
Select("Posts.ChannelId AS channelid", unixSelect, "count(Posts.Id) AS postcount").
From("Posts").
LeftJoin("Channels ON Posts.ChannelId = Channels.Id").
Where(sq.And{
sq.Eq{"Posts.DeleteAt": 0},
sq.Gt{"Posts.CreateAt": sinceUnixMillis},
sq.Eq{"Posts.Type": ""},
sq.Eq{"Channels.Id": channelIDs},
}).
Where(propsQuery).
GroupBy("channelid", "duration").
OrderBy("channelid", "duration")
if userID != nil && model.IsValidId(*userID) {
query = query.Where(sq.And{sq.Eq{"Posts.UserId": *userID}})
}
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "failed to parse query")
}
dailyPostCounts := make([]*model.DurationPostCount, 0)
if err := s.GetReplicaX().Select(&dailyPostCounts, queryString, args...); err != nil {
return nil, errors.Wrap(err, "failed to get post counts by duration")
}
return dailyPostCounts, nil
}

View File

@ -3059,163 +3059,6 @@ func (s *SqlPostStore) updateThreadsFromPosts(transaction *sqlxTxWrapper, posts
return nil
}
func (s *SqlPostStore) GetTopDMsForUserSince(userID string, since int64, offset int, limit int) (*model.TopDMList, error) {
var botsFilterExpr string
/*
Channel.Name is of the format userId1__userId2.
Using this, self dms, and bot dms can be filtered.
*/
if s.DriverName() == model.DatabaseDriverPostgres {
botsFilterExpr = `SPLIT_PART(Channels.Name, '__', 1) NOT IN (SELECT UserId FROM Bots)
AND SPLIT_PART(Channels.Name, '__', 2) NOT IN (SELECT UserId FROM Bots)
`
} else if s.DriverName() == model.DatabaseDriverMysql {
botsFilterExpr = `SUBSTRING_INDEX(Channels.Name, '__', 1) NOT IN (SELECT UserId FROM Bots)
AND SUBSTRING_INDEX(Channels.Name, '__', -1) NOT IN (SELECT UserId FROM Bots)
`
}
channelSelector := s.getQueryBuilder().Select("Id", "TotalMsgCount").From("Channels").Join("ChannelMembers as cm on cm.ChannelId = Channels.Id").
Where(sq.And{
sq.Expr("Channels.Type = 'D'"),
sq.Eq{"cm.UserId": userID},
sq.NotEq{"Channels.Name": fmt.Sprintf("%s__%s", userID, userID)},
sq.Expr(botsFilterExpr),
})
var aggregator string
if s.DriverName() == model.DatabaseDriverMysql {
aggregator = "group_concat(distinct cm.UserId) as Participants"
} else {
aggregator = "string_agg(distinct cm.UserId, ',') as Participants"
}
topDMsBuilder := s.getQueryBuilder().Select("count(p.Id) as MessageCount", aggregator, "vch.Id as ChannelId").FromSelect(channelSelector, "vch").
Join("ChannelMembers as cm on cm.ChannelId = vch.Id").
Join("Posts as p on p.ChannelId = vch.Id").
Where(sq.And{
sq.Gt{
"p.UpdateAt": since,
},
sq.Eq{
"p.DeleteAt": 0,
},
}).GroupBy("vch.id")
// following where clause filters out all archived DMs with "deleted" users, that has only 1 user-id in Participants column.
archivedDMsFilter := s.getQueryBuilder().Select("MessageCount", "Participants", "ChannelId").FromSelect(topDMsBuilder, "top_dms").
Where(sq.Expr("POSITION(',' IN Participants) > 0"))
archivedDMsFilter = archivedDMsFilter.OrderBy("MessageCount DESC").Limit(uint64(limit + 1)).Offset(uint64(offset))
topDMs := make([]*model.TopDM, 0)
sql, args, err := archivedDMsFilter.ToSql()
if err != nil {
return nil, errors.Wrap(err, "GetTopDMsForUserSince_ToSql")
}
err = s.GetReplicaX().Select(&topDMs, sql, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find top DMs for user-id: %s", userID)
}
// fill SecondParticipant column
topDMs, err = postProcessTopDMs(s, userID, topDMs, since)
if err != nil {
return nil, err
}
return model.GetTopDMListWithPagination(topDMs, limit), nil
}
func postProcessTopDMs(s *SqlPostStore, userID string, topDMs []*model.TopDM, since int64) ([]*model.TopDM, error) {
var topDMsFiltered = []*model.TopDM{}
var secondParticipantIds []string
var channelIds []string
// identify second participant in a list of participants
for _, topDM := range topDMs {
participants := strings.Split(topDM.Participants, ",")
var secondParticipantId string
// divide message count by 2, because it's counted twice due to channel memberships being 2 for dms.
topDM.MessageCount = topDM.MessageCount / 2
if participants[0] == userID {
secondParticipantId = participants[1]
} else {
secondParticipantId = participants[0]
}
secondParticipantIds = append(secondParticipantIds, secondParticipantId)
channelIds = append(channelIds, topDM.ChannelId)
}
// get user profiles
users, err := s.User().GetProfileByIds(context.Background(), secondParticipantIds, &store.UserGetByIdsOpts{}, true)
if err != nil {
return nil, errors.Wrapf(err, "failed to get second participants' information")
}
// get outgoing message count for userId
outgoingMessagesQuery := s.getQueryBuilder().Select("ch.Id as ChannelId, count(p.Id) as MessageCount").From("Channels as ch").
Join("Posts as p on p.ChannelId=ch.Id").Where(
sq.And{
sq.Gt{
"p.UpdateAt": since,
},
sq.Eq{
"p.DeleteAt": 0,
},
sq.Eq{
"ch.Id": channelIds,
},
sq.Eq{
"p.UserId": userID,
},
}).GroupBy("ch.Id")
outgoingMessages := make([]*model.OutgoingMessageQueryResult, 0)
sql, args, err := outgoingMessagesQuery.ToSql()
if err != nil {
return nil, errors.Wrap(err, "GetTopDMsForUserSince_outgoingMessagesQuery_ToSql")
}
err = s.GetReplicaX().Select(&outgoingMessages, sql, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to find top DMs for user-id: %s", userID)
}
// create map of channelId -> MessageCount
outgoingMessagesMap := make(map[string]int)
for _, outgoingMessage := range outgoingMessages {
outgoingMessagesMap[outgoingMessage.ChannelId] = outgoingMessage.MessageCount
}
// create map of userId -> User
usersMap := make(map[string]*model.User)
for _, user := range users {
usersMap[user.Id] = user
}
for index, topDM := range topDMs {
if secondParticipantIds[index] == "-1" {
return nil, errors.Wrapf(err, "failed to find second user for topDM: %s", userID)
}
user := usersMap[secondParticipantIds[index]]
topDM.SecondParticipant = &model.TopDMInsightUserInformation{
InsightUserInformation: model.InsightUserInformation{
Id: user.Id,
LastPictureUpdate: user.LastPictureUpdate,
FirstName: user.FirstName,
LastName: user.LastName,
Username: user.Username,
NickName: user.Nickname,
},
Position: user.Position,
}
topDM.OutgoingMessageCount = int64(outgoingMessagesMap[topDM.ChannelId])
topDMsFiltered = append(topDMsFiltered, topDM)
}
return topDMsFiltered, nil
}
func (s *SqlPostStore) SetPostReminder(reminder *model.PostReminder) error {
transaction, err := s.GetMasterX().Beginx()
if err != nil {

View File

@ -245,124 +245,6 @@ func (s *SqlReactionStore) PermanentDeleteBatch(endTime int64, limit int64) (int
return rowsAffected, nil
}
// GetTopForTeamSince returns the instance counts of the following Reactions sets:
// a) those created by anyone in private channels in the given user's membership graph on the given team, and
// b) those created by anyone in public channels on the given team.
func (s *SqlReactionStore) GetTopForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopReactionList, error) {
reactions := make([]*model.TopReaction, 0)
query := `
SELECT
EmojiName,
sum(EmojiCount) AS Count
FROM ((
SELECT
EmojiName,
count(EmojiName) AS EmojiCount,
Reactions.DeleteAt AS DeleteAt,
Reactions.CreateAt AS CreateAt
FROM
ChannelMembers
INNER JOIN Channels ON ChannelMembers.ChannelId = Channels.Id
INNER JOIN Reactions ON Channels.Id = Reactions.ChannelId
WHERE
ChannelMembers.UserId = ?
AND Channels.Type = 'P'
AND Channels.TeamId = ?
GROUP BY
Reactions.EmojiName,
Reactions.DeleteAt,
Reactions.CreateAt)
UNION ALL (
SELECT
EmojiName,
count(EmojiName) AS EmojiCount,
Reactions.DeleteAt AS DeleteAt,
Reactions.CreateAt AS CreateAt
FROM
Reactions
INNER JOIN PublicChannels ON Reactions.ChannelId = PublicChannels.Id
WHERE
PublicChannels.TeamId = ?
GROUP BY
Reactions.EmojiName,
Reactions.DeleteAt,
Reactions.CreateAt)) AS A
WHERE
DeleteAt = 0
AND CreateAt > ?
GROUP BY
EmojiName
ORDER BY
Count DESC,
EmojiName ASC
LIMIT ?
OFFSET ?`
if err := s.GetReplicaX().Select(&reactions, query, userID, teamID, teamID, since, limit+1, offset); err != nil {
return nil, errors.Wrap(err, "failed to get top Reactions")
}
return model.GetTopReactionListWithPagination(reactions, limit), nil
}
// GetTopForUserSince returns the instance counts of the following Reactions sets:
// a) those created by the given user in any channel type on the given team (across the workspace if no team is given), and
// b) those created by the given user in DM or group channels.
func (s *SqlReactionStore) GetTopForUserSince(userID string, teamID string, since int64, offset int, limit int) (*model.TopReactionList, error) {
reactions := make([]*model.TopReaction, 0)
var args []any
var query string
if teamID != "" {
query = `
SELECT
EmojiName,
count(EmojiName) AS Count
FROM
Reactions
INNER JOIN Channels ON Channels.Id = Reactions.ChannelId
WHERE
Reactions.DeleteAt = 0
AND Reactions.UserId = ?
AND (Channels.TeamId = ? OR Channels.Type = 'D' OR Channels.Type = 'G')
AND Reactions.CreateAt > ?
GROUP BY
EmojiName
ORDER BY
Count DESC,
EmojiName ASC
LIMIT ?
OFFSET ?`
args = []any{userID, teamID, since, limit + 1, offset}
} else {
query = `
SELECT
EmojiName,
count(EmojiName) AS Count
FROM
Reactions
WHERE
Reactions.DeleteAt = 0
AND Reactions.UserId = ?
AND Reactions.CreateAt > ?
GROUP BY
Reactions.EmojiName
ORDER BY
Count DESC,
EmojiName ASC
LIMIT ?
OFFSET ?`
args = []any{userID, since, limit + 1, offset}
}
if err := s.GetReplicaX().Select(&reactions, query, args...); err != nil {
return nil, errors.Wrap(err, "failed to get top Reactions")
}
return model.GetTopReactionListWithPagination(reactions, limit), nil
}
func (s *SqlReactionStore) saveReactionAndUpdatePost(transaction *sqlxTxWrapper, reaction *model.Reaction) error {
reaction.DeleteAt = 0

View File

@ -1652,46 +1652,3 @@ func (s SqlTeamStore) GroupSyncedTeamCount() (int64, error) {
return count, nil
}
func (s SqlTeamStore) GetNewTeamMembersSince(teamID string, since int64, offset int, limit int, showFullName bool) (*model.NewTeamMembersList, int64, error) {
builderF := func(selectClause string) sq.SelectBuilder {
return s.getQueryBuilder().
Select(selectClause).
From("TeamMembers").
Join("Users ON Users.id = TeamMembers.userid").
LeftJoin("Bots ON Bots.userid = Users.id").
Where(sq.GtOrEq{"TeamMembers.createat": since}).
Where(sq.Eq{"TeamMembers.deleteat": 0, "teamid": teamID, "Users.deleteat": 0, "Bots.userid": nil})
}
countBuilder := builderF("count(*)")
query, args, err := countBuilder.ToSql()
if err != nil {
return nil, 0, errors.Wrap(err, "team_tosql")
}
var totalCount int64
err = s.GetReplicaX().Get(&totalCount, query, args...)
if err != nil {
return nil, 0, errors.Wrap(err, "failed to count team members since")
}
selectClause := "Users.Id, Users.Username, Users.Position, Users.LastPictureUpdate, TeamMembers.CreateAt, Users.Nickname"
if showFullName {
selectClause += ", Users.FirstName, Users.LastName"
}
newTeamMembersBuilder := builderF(selectClause).
Limit(uint64(limit + 1)).
Offset(uint64(offset))
query, args, err = newTeamMembersBuilder.ToSql()
if err != nil {
return nil, 0, errors.Wrap(err, "team_tosql")
}
var ntms []*model.NewTeamMember
err = s.GetReplicaX().Select(&ntms, query, args...)
if err != nil {
return nil, 0, errors.Wrap(err, "failed to get team members since")
}
return model.GetNewTeamMembersListWithPagination(ntms, limit), totalCount, nil
}

View File

@ -1021,209 +1021,3 @@ func (s *SqlThreadStore) GetThreadUnreadReplyCount(threadMembership *model.Threa
return unreadReplies, nil
}
// Top threads in all public channels and private channels userID is a member of. Returns a list of threads ranked by interactions.
func (s *SqlThreadStore) GetTopThreadsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
var args []any
query := `select
threads_list.PostId,
threads_list.ReplyCount,
threads_list.ChannelId,
threads_list.DisplayName,
threads_list.Name,
threads_list.Participants,
p.UserId
from((
SELECT
t.PostId,
t.ReplyCount,
t.ChannelId,
t.Participants,
c.DisplayName,
c.Name
FROM
Threads t
LEFT JOIN PublicChannels c ON t.ChannelId = c.Id
WHERE
t.threaddeleteat IS NULL
AND t.LastReplyAt > ?
AND c.TeamId = ?
GROUP BY
t.PostId,
c.DisplayName,
c.Name,
t.Participants
)
UNION
ALL (
SELECT
t.PostId,
t.ReplyCount,
t.ChannelId,
t.Participants,
c.DisplayName,
c.Name
FROM
Threads t
LEFT JOIN ChannelMembers cm ON t.ChannelId = cm.ChannelId
LEFT JOIN Channels c ON t.ChannelId = c.Id
WHERE
t.threaddeleteat IS NULL
AND cm.UserId = ?
AND c.Type = 'P'
AND c.TeamId = ?
AND t.LastReplyAt > ?
GROUP BY
t.PostId,
c.DisplayName,
c.Name,
t.Participants
)) as threads_list
LEFT JOIN Posts as p on p.Id = threads_list.PostId
ORDER BY ReplyCount DESC
limit ? offset ?`
args = append(args, since, teamID, userID, teamID, since, limit+1, offset)
topThreads := make([]*model.TopThread, 0)
err := s.GetReplicaX().Select(&topThreads, query, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to get top threads=%s", teamID)
}
topThreads, err = postProcessTopThreads(topThreads, s, teamID)
if err != nil {
return nil, err
}
return model.GetTopThreadListWithPagination(topThreads, limit), nil
}
func (s *SqlThreadStore) GetTopThreadsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
var args []any
// gets all threads within the team which user follows.
query := `select
threads_list.PostId,
threads_list.ReplyCount,
threads_list.ChannelId,
threads_list.DisplayName,
threads_list.Name,
threads_list.Participants,
p.UserId
from((
SELECT
t.PostId,
t.ReplyCount,
t.ChannelId,
t.Participants,
c.DisplayName,
c.Name
FROM
Threads t
LEFT JOIN PublicChannels c ON t.ChannelId = c.Id
LEFT JOIN ThreadMemberships as tm on t.PostId = tm.PostId
WHERE
t.threaddeleteat IS NULL
AND t.LastReplyAt > ?
AND c.TeamId = ?
AND tm.UserId = ?
AND tm.Following = TRUE
GROUP BY
t.PostId,
c.DisplayName,
c.Name,
t.Participants
)
UNION
ALL (
SELECT
t.PostId,
t.ReplyCount,
t.ChannelId,
t.Participants,
c.DisplayName,
c.Name
FROM
Threads t
LEFT JOIN ChannelMembers cm ON t.ChannelId = cm.ChannelId
LEFT JOIN Channels c ON t.ChannelId = c.Id
LEFT JOIN ThreadMemberships as tm on t.PostId = tm.PostId
WHERE
cm.UserId = ?
AND c.Type = 'P'
AND c.TeamId = ?
AND t.threaddeleteat IS NULL
AND t.LastReplyAt > ?
AND tm.UserId = ?
AND tm.Following = TRUE
GROUP BY
t.PostId,
c.DisplayName,
c.Name,
t.Participants
)) as threads_list
LEFT JOIN Posts as p on p.Id = threads_list.PostId
ORDER BY ReplyCount DESC
limit ? offset ?`
args = append(args, since, teamID, userID, userID, teamID, since, userID, limit+1, offset)
topThreads := make([]*model.TopThread, 0)
err := s.GetReplicaX().Select(&topThreads, query, args...)
if err != nil {
return nil, errors.Wrapf(err, "failed to get top threads=%s", teamID)
}
topThreads, err = postProcessTopThreads(topThreads, s, teamID)
if err != nil {
return nil, err
}
return model.GetTopThreadListWithPagination(topThreads, limit), nil
}
func userContains(userIDs []string, searchedUserID string) bool {
for _, userID := range userIDs {
if userID == searchedUserID {
return true
}
}
return false
}
func postProcessTopThreads(topThreads []*model.TopThread, s *SqlThreadStore, teamID string) ([]*model.TopThread, error) {
// create list of userIDs
var userIDs []string
for _, topThread := range topThreads {
userID := topThread.UserId
if !userContains(userIDs, userID) {
userIDs = append(userIDs, userID)
}
}
usersMap := map[string]*model.User{}
users, err := s.User().GetProfileByIds(context.Background(), userIDs, &store.UserGetByIdsOpts{}, true)
if err != nil {
return nil, errors.Wrapf(err, "failed to get users for top threads in team=%s", teamID)
}
for _, user := range users {
usersMap[user.Id] = user
}
// resolve user, root post for each top thread
for _, topThread := range topThreads {
postCreator := usersMap[topThread.UserId]
topThread.UserInformation = &model.InsightUserInformation{
Id: postCreator.Id,
LastPictureUpdate: postCreator.LastPictureUpdate,
FirstName: postCreator.FirstName,
LastName: postCreator.LastName,
Username: postCreator.Username,
NickName: postCreator.Nickname,
}
post, err := s.Post().GetSingle(topThread.PostId, false)
if err != nil {
return nil, errors.Wrapf(err, "failed to get extended post for post id=%s", topThread.PostId)
}
topThread.Post = post
}
return topThreads, nil
}

View File

@ -170,8 +170,6 @@ type TeamStore interface {
// GetCommonTeamIDsForTwoUsers returns the intersection of all the teams to which the specified
// users belong.
GetCommonTeamIDsForTwoUsers(userID, otherUserID string) ([]string, error)
GetNewTeamMembersSince(teamID string, since int64, offset int, limit int, showFullName bool) (*model.NewTeamMembersList, int64, error)
}
type ChannelStore interface {
@ -300,15 +298,6 @@ type ChannelStore interface {
SetShared(channelId string, shared bool) error
// GetTeamForChannel returns the team for a given channelID.
GetTeamForChannel(channelID string) (*model.Team, error)
// Insights - channels
GetTopChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopChannelList, error)
GetTopChannelsForUserSince(userID string, teamID string, since int64, offset int, limit int) (*model.TopChannelList, error)
PostCountsByDuration(channelIDs []string, sinceUnixMillis int64, userID *string, duration model.PostCountGrouping, groupingLocation *time.Location) ([]*model.DurationPostCount, error)
// Insights - inactive channels
GetTopInactiveChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error)
GetTopInactiveChannelsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error)
}
type ChannelMemberHistoryStore interface {
@ -347,10 +336,6 @@ type ThreadStore interface {
DeleteOrphanedRows(limit int) (deleted int64, err error)
GetThreadUnreadReplyCount(threadMembership *model.ThreadMembership) (int64, error)
DeleteMembershipsForChannel(userID, channelID string) error
// Insights - threads
GetTopThreadsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error)
GetTopThreadsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error)
}
type PostStore interface {
@ -404,9 +389,6 @@ type PostStore interface {
GetPostReminderMetadata(postID string) (*PostReminderMetadata, error)
// GetNthRecentPostTime returns the CreateAt time of the nth most recent post.
GetNthRecentPostTime(n int64) (int64, error)
// Insights - top DMs
GetTopDMsForUserSince(userID string, since int64, offset int, limit int) (*model.TopDMList, error)
}
type UserStore interface {
@ -727,8 +709,6 @@ type ReactionStore interface {
BulkGetForPosts(postIds []string) ([]*model.Reaction, error)
DeleteOrphanedRows(limit int) (int64, error)
PermanentDeleteBatch(endTime int64, limit int64) (int64, error)
GetTopForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopReactionList, error)
GetTopForUserSince(userID string, teamID string, since int64, offset int, limit int) (*model.TopReactionList, error)
}
type JobStore interface {

View File

@ -149,8 +149,6 @@ func TestChannelStore(t *testing.T, ss store.Store, s SqlStore) {
t.Run("UpdateSidebarChannelsByPreferences", func(t *testing.T) { testUpdateSidebarChannelsByPreferences(t, ss) })
t.Run("SetShared", func(t *testing.T) { testSetShared(t, ss) })
t.Run("GetTeamForChannel", func(t *testing.T) { testGetTeamForChannel(t, ss) })
t.Run("PostCountsByDuration", func(t *testing.T) { testChannelPostCountsByDuration(t, ss) })
t.Run("GetTopInactiveChannels", func(t *testing.T) { testGetTopInactiveChannels(t, ss) })
}
func testChannelStoreSave(t *testing.T, ss store.Store) {
@ -8067,235 +8065,3 @@ func testGetTeamForChannel(t *testing.T, ss store.Store) {
var nfErr *store.ErrNotFound
require.True(t, errors.As(err, &nfErr))
}
func testChannelPostCountsByDuration(t *testing.T, ss store.Store) {
team, err := ss.Team().Save(&model.Team{
Name: model.NewId(),
DisplayName: "DisplayName",
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
defer func() { ss.Team().PermanentDelete(team.Id) }()
channel := &model.Channel{
TeamId: team.Id,
DisplayName: "test_share_flag",
Name: "test_share_flag",
Type: model.ChannelTypeOpen,
}
channelSaved, err := ss.Channel().Save(channel, 999)
require.NoError(t, err)
defer func() { ss.Channel().PermanentDelete(channelSaved.Id) }()
userID := model.NewId()
_, err = ss.Post().Save(&model.Post{
UserId: userID,
ChannelId: channel.Id,
Message: "test",
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
UserId: userID,
ChannelId: channel.Id,
Message: "test",
Props: model.StringInterface{
"from_bot": true,
},
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
UserId: userID,
ChannelId: channel.Id,
Message: "test",
Props: model.StringInterface{
"from_webhook": true,
},
})
require.NoError(t, err)
dpc, err := ss.Channel().PostCountsByDuration([]string{channelSaved.Id}, 0, &userID, model.PostsByDay, time.Now().Location())
require.NoError(t, err)
require.Len(t, dpc, 1)
require.Equal(t, channel.Id, dpc[0].ChannelID)
require.Equal(t, 1, dpc[0].PostCount)
}
func testGetTopInactiveChannels(t *testing.T, ss store.Store) {
team, err := ss.Team().Save(&model.Team{
Name: model.NewId(),
DisplayName: "DisplayName",
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
defer func() { ss.Team().PermanentDelete(team.Id) }()
channelPublic0 := &model.Channel{
TeamId: team.Id,
DisplayName: "test_share_flag asdf",
Name: "test_share_flag_public0",
Type: model.ChannelTypeOpen,
CreateAt: 1,
}
channelSaved0, err := ss.Channel().Save(channelPublic0, 999)
require.NoError(t, err)
defer func() { ss.Channel().PermanentDelete(channelSaved0.Id) }()
channelPublic1 := &model.Channel{
TeamId: team.Id,
DisplayName: "test_share_flag",
Name: "test_share_flag",
Type: model.ChannelTypeOpen,
CreateAt: 1,
}
channelSaved1, err := ss.Channel().Save(channelPublic1, 999)
require.NoError(t, err)
defer func() { ss.Channel().PermanentDelete(channelSaved1.Id) }()
// create private channel
c3 := model.Channel{}
c3.TeamId = team.Id
c3.DisplayName = "Channel3" + model.NewId()
c3.Name = NewTestId()
c3.Type = model.ChannelTypePrivate
c3.CreateAt = 1
channelPrivate, nErr := ss.Channel().Save(&c3, -1)
require.NoError(t, nErr)
// create private channel with post
c3NoPost := model.Channel{}
c3NoPost.TeamId = team.Id
c3NoPost.DisplayName = "Channel3" + model.NewId()
c3NoPost.Name = NewTestId()
c3NoPost.Type = model.ChannelTypePrivate
c3NoPost.CreateAt = 1
channelPrivateNoPost, nErr := ss.Channel().Save(&c3NoPost, -1)
require.NoError(t, nErr)
// create dm channel
u1 := model.User{}
u1.Email = MakeEmail()
u1.Nickname = model.NewId()
_, err = ss.User().Save(&u1)
require.NoError(t, err)
u2 := model.User{}
u2.Email = MakeEmail()
u2.Nickname = model.NewId()
_, err = ss.User().Save(&u2)
require.NoError(t, err)
uBot := model.User{Id: model.NewId()}
_, nErr = ss.Channel().CreateDirectChannel(&u1, &u2)
require.NoError(t, nErr)
// add u1, u2 to channels
cm1 := &model.ChannelMember{ChannelId: channelPrivate.Id, UserId: u1.Id, NotifyProps: model.GetDefaultChannelNotifyProps()}
_, err = ss.Channel().SaveMember(cm1)
require.NoError(t, err)
cm1NoPost := &model.ChannelMember{ChannelId: channelPrivateNoPost.Id, UserId: u1.Id, NotifyProps: model.GetDefaultChannelNotifyProps()}
_, err = ss.Channel().SaveMember(cm1NoPost)
require.NoError(t, err)
cm1Public := &model.ChannelMember{ChannelId: channelPublic1.Id, UserId: u1.Id, NotifyProps: model.GetDefaultChannelNotifyProps()}
_, err = ss.Channel().SaveMember(cm1Public)
require.NoError(t, err)
cm2 := &model.ChannelMember{ChannelId: channelPublic0.Id, UserId: u2.Id, NotifyProps: model.GetDefaultChannelNotifyProps()}
_, err = ss.Channel().SaveMember(cm2)
require.NoError(t, err)
cmBot := &model.ChannelMember{ChannelId: channelPublic0.Id, UserId: uBot.Id, NotifyProps: model.GetDefaultChannelNotifyProps()}
_, err = ss.Channel().SaveMember(cmBot)
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
UserId: u1.Id,
ChannelId: channelPrivate.Id,
Message: "test",
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
UserId: u1.Id,
ChannelId: channelPrivate.Id,
Message: "test1",
})
require.NoError(t, err)
// create posts in channel public 0
postToCheckLastUpdateAt, err := ss.Post().Save(&model.Post{
UserId: u2.Id,
ChannelId: channelSaved0.Id,
Message: "test",
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
UserId: model.NewId(),
ChannelId: channelPublic1.Id,
Message: "test",
Props: model.StringInterface{
"from_bot": true,
},
})
require.NoError(t, err)
// create posts in channel public 1
for i := 0; i < 3; i++ {
_, err = ss.Post().Save(&model.Post{
UserId: model.NewId(),
ChannelId: channelPublic1.Id,
Message: "test",
})
require.NoError(t, err)
}
// for u1
t.Run("top inactive channels for team - u1 ", func(t *testing.T) {
topInactiveChannels, err := ss.Channel().GetTopInactiveChannelsForTeamSince(team.Id, u1.Id, 2, 0, 10)
require.NoError(t, err)
require.Len(t, topInactiveChannels.Items, 4)
require.Equal(t, topInactiveChannels.Items[0].ID, channelPrivateNoPost.Id)
require.Equal(t, topInactiveChannels.Items[1].ID, channelSaved0.Id)
require.Equal(t, topInactiveChannels.Items[1].LastActivityAt, postToCheckLastUpdateAt.CreateAt)
require.Equal(t, topInactiveChannels.Items[2].ID, channelPrivate.Id)
require.Equal(t, topInactiveChannels.Items[3].ID, channelPublic1.Id)
// test bot posts are counted
require.Equal(t, topInactiveChannels.Items[3].MessageCount, int64(4))
// participants
require.Equal(t, topInactiveChannels.Items[2].Participants[0], u1.Id)
require.Equal(t, topInactiveChannels.Items[3].Participants[0], u1.Id)
})
t.Run("top inactive channels for user - u1 ", func(t *testing.T) {
topInactiveChannels, err := ss.Channel().GetTopInactiveChannelsForUserSince(team.Id, u1.Id, 2, 0, 10)
require.NoError(t, err)
require.Len(t, topInactiveChannels.Items, 3)
require.Equal(t, topInactiveChannels.Items[0].ID, channelPrivateNoPost.Id)
require.Equal(t, topInactiveChannels.Items[1].ID, channelPrivate.Id)
require.Equal(t, topInactiveChannels.Items[2].ID, channelPublic1.Id)
})
// for u2
t.Run("top inactive channels for team - u2 ", func(t *testing.T) {
topInactiveChannels, err := ss.Channel().GetTopInactiveChannelsForTeamSince(team.Id, u2.Id, 2, 0, 10)
require.NoError(t, err)
require.Len(t, topInactiveChannels.Items, 2)
require.Equal(t, topInactiveChannels.Items[0].ID, channelSaved0.Id)
require.Equal(t, topInactiveChannels.Items[0].LastActivityAt, postToCheckLastUpdateAt.CreateAt)
require.Equal(t, topInactiveChannels.Items[1].ID, channelPublic1.Id)
})
t.Run("top inactive channels for user - u2 ", func(t *testing.T) {
topInactiveChannels, err := ss.Channel().GetTopInactiveChannelsForUserSince(team.Id, u2.Id, 2, 0, 10)
require.NoError(t, err)
require.Len(t, topInactiveChannels.Items, 1)
require.Equal(t, topInactiveChannels.Items[0].ID, channelPublic0.Id)
})
}

View File

@ -11,8 +11,6 @@ import (
mock "github.com/stretchr/testify/mock"
store "github.com/mattermost/mattermost/server/v8/channels/store"
time "time"
)
// ChannelStore is an autogenerated mock type for the ChannelStore type
@ -1774,110 +1772,6 @@ func (_m *ChannelStore) GetTeamMembersForChannel(channelID string) ([]string, er
return r0, r1
}
// GetTopChannelsForTeamSince provides a mock function with given fields: teamID, userID, since, offset, limit
func (_m *ChannelStore) GetTopChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopChannelList, error) {
ret := _m.Called(teamID, userID, since, offset, limit)
var r0 *model.TopChannelList
var r1 error
if rf, ok := ret.Get(0).(func(string, string, int64, int, int) (*model.TopChannelList, error)); ok {
return rf(teamID, userID, since, offset, limit)
}
if rf, ok := ret.Get(0).(func(string, string, int64, int, int) *model.TopChannelList); ok {
r0 = rf(teamID, userID, since, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TopChannelList)
}
}
if rf, ok := ret.Get(1).(func(string, string, int64, int, int) error); ok {
r1 = rf(teamID, userID, since, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTopChannelsForUserSince provides a mock function with given fields: userID, teamID, since, offset, limit
func (_m *ChannelStore) GetTopChannelsForUserSince(userID string, teamID string, since int64, offset int, limit int) (*model.TopChannelList, error) {
ret := _m.Called(userID, teamID, since, offset, limit)
var r0 *model.TopChannelList
var r1 error
if rf, ok := ret.Get(0).(func(string, string, int64, int, int) (*model.TopChannelList, error)); ok {
return rf(userID, teamID, since, offset, limit)
}
if rf, ok := ret.Get(0).(func(string, string, int64, int, int) *model.TopChannelList); ok {
r0 = rf(userID, teamID, since, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TopChannelList)
}
}
if rf, ok := ret.Get(1).(func(string, string, int64, int, int) error); ok {
r1 = rf(userID, teamID, since, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTopInactiveChannelsForTeamSince provides a mock function with given fields: teamID, userID, since, offset, limit
func (_m *ChannelStore) GetTopInactiveChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error) {
ret := _m.Called(teamID, userID, since, offset, limit)
var r0 *model.TopInactiveChannelList
var r1 error
if rf, ok := ret.Get(0).(func(string, string, int64, int, int) (*model.TopInactiveChannelList, error)); ok {
return rf(teamID, userID, since, offset, limit)
}
if rf, ok := ret.Get(0).(func(string, string, int64, int, int) *model.TopInactiveChannelList); ok {
r0 = rf(teamID, userID, since, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TopInactiveChannelList)
}
}
if rf, ok := ret.Get(1).(func(string, string, int64, int, int) error); ok {
r1 = rf(teamID, userID, since, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTopInactiveChannelsForUserSince provides a mock function with given fields: teamID, userID, since, offset, limit
func (_m *ChannelStore) GetTopInactiveChannelsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error) {
ret := _m.Called(teamID, userID, since, offset, limit)
var r0 *model.TopInactiveChannelList
var r1 error
if rf, ok := ret.Get(0).(func(string, string, int64, int, int) (*model.TopInactiveChannelList, error)); ok {
return rf(teamID, userID, since, offset, limit)
}
if rf, ok := ret.Get(0).(func(string, string, int64, int, int) *model.TopInactiveChannelList); ok {
r0 = rf(teamID, userID, since, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TopInactiveChannelList)
}
}
if rf, ok := ret.Get(1).(func(string, string, int64, int, int) error); ok {
r1 = rf(teamID, userID, since, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GroupSyncedChannelCount provides a mock function with given fields:
func (_m *ChannelStore) GroupSyncedChannelCount() (int64, error) {
ret := _m.Called()
@ -2047,32 +1941,6 @@ func (_m *ChannelStore) PermanentDeleteMembersByUser(userID string) error {
return r0
}
// PostCountsByDuration provides a mock function with given fields: channelIDs, sinceUnixMillis, userID, duration, groupingLocation
func (_m *ChannelStore) PostCountsByDuration(channelIDs []string, sinceUnixMillis int64, userID *string, duration model.PostCountGrouping, groupingLocation *time.Location) ([]*model.DurationPostCount, error) {
ret := _m.Called(channelIDs, sinceUnixMillis, userID, duration, groupingLocation)
var r0 []*model.DurationPostCount
var r1 error
if rf, ok := ret.Get(0).(func([]string, int64, *string, model.PostCountGrouping, *time.Location) ([]*model.DurationPostCount, error)); ok {
return rf(channelIDs, sinceUnixMillis, userID, duration, groupingLocation)
}
if rf, ok := ret.Get(0).(func([]string, int64, *string, model.PostCountGrouping, *time.Location) []*model.DurationPostCount); ok {
r0 = rf(channelIDs, sinceUnixMillis, userID, duration, groupingLocation)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.DurationPostCount)
}
}
if rf, ok := ret.Get(1).(func([]string, int64, *string, model.PostCountGrouping, *time.Location) error); ok {
r1 = rf(channelIDs, sinceUnixMillis, userID, duration, groupingLocation)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// RemoveAllDeactivatedMembers provides a mock function with given fields: channelID
func (_m *ChannelStore) RemoveAllDeactivatedMembers(channelID string) error {
ret := _m.Called(channelID)

View File

@ -840,32 +840,6 @@ func (_m *PostStore) GetSingle(id string, inclDeleted bool) (*model.Post, error)
return r0, r1
}
// GetTopDMsForUserSince provides a mock function with given fields: userID, since, offset, limit
func (_m *PostStore) GetTopDMsForUserSince(userID string, since int64, offset int, limit int) (*model.TopDMList, error) {
ret := _m.Called(userID, since, offset, limit)
var r0 *model.TopDMList
var r1 error
if rf, ok := ret.Get(0).(func(string, int64, int, int) (*model.TopDMList, error)); ok {
return rf(userID, since, offset, limit)
}
if rf, ok := ret.Get(0).(func(string, int64, int, int) *model.TopDMList); ok {
r0 = rf(userID, since, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TopDMList)
}
}
if rf, ok := ret.Get(1).(func(string, int64, int, int) error); ok {
r1 = rf(userID, since, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// HasAutoResponsePostByUserSince provides a mock function with given fields: options, userId
func (_m *PostStore) HasAutoResponsePostByUserSince(options model.GetPostsSinceOptions, userId string) (bool, error) {
ret := _m.Called(options, userId)

View File

@ -156,58 +156,6 @@ func (_m *ReactionStore) GetForPostSince(postId string, since int64, excludeRemo
return r0, r1
}
// GetTopForTeamSince provides a mock function with given fields: teamID, userID, since, offset, limit
func (_m *ReactionStore) GetTopForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopReactionList, error) {
ret := _m.Called(teamID, userID, since, offset, limit)
var r0 *model.TopReactionList
var r1 error
if rf, ok := ret.Get(0).(func(string, string, int64, int, int) (*model.TopReactionList, error)); ok {
return rf(teamID, userID, since, offset, limit)
}
if rf, ok := ret.Get(0).(func(string, string, int64, int, int) *model.TopReactionList); ok {
r0 = rf(teamID, userID, since, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TopReactionList)
}
}
if rf, ok := ret.Get(1).(func(string, string, int64, int, int) error); ok {
r1 = rf(teamID, userID, since, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTopForUserSince provides a mock function with given fields: userID, teamID, since, offset, limit
func (_m *ReactionStore) GetTopForUserSince(userID string, teamID string, since int64, offset int, limit int) (*model.TopReactionList, error) {
ret := _m.Called(userID, teamID, since, offset, limit)
var r0 *model.TopReactionList
var r1 error
if rf, ok := ret.Get(0).(func(string, string, int64, int, int) (*model.TopReactionList, error)); ok {
return rf(userID, teamID, since, offset, limit)
}
if rf, ok := ret.Get(0).(func(string, string, int64, int, int) *model.TopReactionList); ok {
r0 = rf(userID, teamID, since, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TopReactionList)
}
}
if rf, ok := ret.Get(1).(func(string, string, int64, int, int) error); ok {
r1 = rf(userID, teamID, since, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// PermanentDeleteBatch provides a mock function with given fields: endTime, limit
func (_m *ReactionStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
ret := _m.Called(endTime, limit)

View File

@ -549,39 +549,6 @@ func (_m *TeamStore) GetMembersByIds(teamID string, userIds []string, restrictio
return r0, r1
}
// GetNewTeamMembersSince provides a mock function with given fields: teamID, since, offset, limit, showFullName
func (_m *TeamStore) GetNewTeamMembersSince(teamID string, since int64, offset int, limit int, showFullName bool) (*model.NewTeamMembersList, int64, error) {
ret := _m.Called(teamID, since, offset, limit, showFullName)
var r0 *model.NewTeamMembersList
var r1 int64
var r2 error
if rf, ok := ret.Get(0).(func(string, int64, int, int, bool) (*model.NewTeamMembersList, int64, error)); ok {
return rf(teamID, since, offset, limit, showFullName)
}
if rf, ok := ret.Get(0).(func(string, int64, int, int, bool) *model.NewTeamMembersList); ok {
r0 = rf(teamID, since, offset, limit, showFullName)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.NewTeamMembersList)
}
}
if rf, ok := ret.Get(1).(func(string, int64, int, int, bool) int64); ok {
r1 = rf(teamID, since, offset, limit, showFullName)
} else {
r1 = ret.Get(1).(int64)
}
if rf, ok := ret.Get(2).(func(string, int64, int, int, bool) error); ok {
r2 = rf(teamID, since, offset, limit, showFullName)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// GetTeamMembersForExport provides a mock function with given fields: userID
func (_m *TeamStore) GetTeamMembersForExport(userID string) ([]*model.TeamMemberForExport, error) {
ret := _m.Called(userID)

View File

@ -273,58 +273,6 @@ func (_m *ThreadStore) GetThreadsForUser(userId string, teamID string, opts mode
return r0, r1
}
// GetTopThreadsForTeamSince provides a mock function with given fields: teamID, userID, since, offset, limit
func (_m *ThreadStore) GetTopThreadsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
ret := _m.Called(teamID, userID, since, offset, limit)
var r0 *model.TopThreadList
var r1 error
if rf, ok := ret.Get(0).(func(string, string, int64, int, int) (*model.TopThreadList, error)); ok {
return rf(teamID, userID, since, offset, limit)
}
if rf, ok := ret.Get(0).(func(string, string, int64, int, int) *model.TopThreadList); ok {
r0 = rf(teamID, userID, since, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TopThreadList)
}
}
if rf, ok := ret.Get(1).(func(string, string, int64, int, int) error); ok {
r1 = rf(teamID, userID, since, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTopThreadsForUserSince provides a mock function with given fields: teamID, userID, since, offset, limit
func (_m *ThreadStore) GetTopThreadsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
ret := _m.Called(teamID, userID, since, offset, limit)
var r0 *model.TopThreadList
var r1 error
if rf, ok := ret.Get(0).(func(string, string, int64, int, int) (*model.TopThreadList, error)); ok {
return rf(teamID, userID, since, offset, limit)
}
if rf, ok := ret.Get(0).(func(string, string, int64, int, int) *model.TopThreadList); ok {
r0 = rf(teamID, userID, since, offset, limit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.TopThreadList)
}
}
if rf, ok := ret.Get(1).(func(string, string, int64, int, int) error); ok {
r1 = rf(teamID, userID, since, offset, limit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTotalThreads provides a mock function with given fields: userId, teamID, opts
func (_m *ThreadStore) GetTotalThreads(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
ret := _m.Called(userId, teamID, opts)

View File

@ -62,7 +62,6 @@ func TestPostStore(t *testing.T, ss store.Store, s SqlStore) {
t.Run("GetPostReminders", func(t *testing.T) { testGetPostReminders(t, ss, s) })
t.Run("GetPostReminderMetadata", func(t *testing.T) { testGetPostReminderMetadata(t, ss, s) })
t.Run("GetNthRecentPostTime", func(t *testing.T) { testGetNthRecentPostTime(t, ss) })
t.Run("GetTopDMsForUserSince", func(t *testing.T) { testGetTopDMsForUserSince(t, ss, s) })
t.Run("GetEditHistoryForPost", func(t *testing.T) { testGetEditHistoryForPost(t, ss) })
}
@ -4862,180 +4861,6 @@ func testGetNthRecentPostTime(t *testing.T, ss store.Store) {
assert.IsType(t, &store.ErrNotFound{}, err)
}
func testGetTopDMsForUserSince(t *testing.T, ss store.Store, s SqlStore) {
// users
user := model.User{Email: MakeEmail(), Username: model.NewId()}
u1 := model.User{Email: MakeEmail(), Username: model.NewId()}
u2 := model.User{Email: MakeEmail(), Username: model.NewId()}
u3 := model.User{Email: MakeEmail(), Username: model.NewId()}
u4 := model.User{Email: MakeEmail(), Username: model.NewId()}
u5 := model.User{Email: MakeEmail(), Username: model.NewId()}
_, err := ss.User().Save(&user)
require.NoError(t, err)
_, err = ss.User().Save(&u1)
require.NoError(t, err)
_, err = ss.User().Save(&u2)
require.NoError(t, err)
_, err = ss.User().Save(&u3)
require.NoError(t, err)
_, err = ss.User().Save(&u4)
require.NoError(t, err)
_, err = ss.User().Save(&u5)
require.NoError(t, err)
bot := &model.Bot{
Username: "bot_user",
Description: "bot",
OwnerId: model.NewId(),
UserId: u5.Id,
}
savedBot, nErr := ss.Bot().Save(bot)
require.NoError(t, nErr)
// user direct messages
chUser1, nErr := ss.Channel().CreateDirectChannel(&u1, &user)
require.NoError(t, nErr)
chUser2, nErr := ss.Channel().CreateDirectChannel(&u2, &user)
require.NoError(t, nErr)
chUser3, nErr := ss.Channel().CreateDirectChannel(&u3, &user)
require.NoError(t, nErr)
// other user direct message
chUser3User4, nErr := ss.Channel().CreateDirectChannel(&u3, &u4)
require.NoError(t, nErr)
// bot direct message - should be ignored by top DMs
botUser, err := ss.User().Get(context.Background(), savedBot.UserId)
require.NoError(t, err)
chBot, nErr := ss.Channel().CreateDirectChannel(&user, botUser)
require.NoError(t, nErr)
_, err = ss.Post().Save(&model.Post{
ChannelId: chBot.Id,
UserId: botUser.Id,
})
require.NoError(t, err)
// sample post data
// for u1
_, err = ss.Post().Save(&model.Post{
ChannelId: chUser1.Id,
UserId: u1.Id,
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
ChannelId: chUser1.Id,
UserId: user.Id,
})
require.NoError(t, err)
// for u2: 1 post
postToDelete, err := ss.Post().Save(&model.Post{
ChannelId: chUser2.Id,
UserId: u2.Id,
})
require.NoError(t, err)
// create second post for u2: modify create at to a very old date to make sure it isn't counted
_, err = ss.Post().Save(&model.Post{
ChannelId: chUser2.Id,
UserId: u2.Id,
CreateAt: 100,
})
require.NoError(t, err)
// for user-u3: 3 posts
for i := 0; i < 3; i++ {
_, err = ss.Post().Save(&model.Post{
ChannelId: chUser3.Id,
UserId: user.Id,
})
require.NoError(t, err)
}
// for u4-u3: 4 posts
u3u4Post1, err := ss.Post().Save(&model.Post{
ChannelId: chUser3User4.Id,
UserId: u3.Id,
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
ChannelId: chUser3User4.Id,
UserId: u4.Id,
})
require.NoError(t, err)
u3u4Post2, err := ss.Post().Save(&model.Post{
ChannelId: chUser3User4.Id,
UserId: u3.Id,
})
require.NoError(t, err)
_, err = ss.Post().Save(&model.Post{
ChannelId: chUser3User4.Id,
UserId: u4.Id,
})
require.NoError(t, err)
t.Run("should return topDMs when userid is specified ", func(t *testing.T) {
topDMs, storeErr := ss.Post().GetTopDMsForUserSince(user.Id, 100, 0, 100)
require.NoError(t, storeErr)
// len of topDMs.Items should be 3
require.Len(t, topDMs.Items, 3)
// check order, magnitude of items
require.Equal(t, topDMs.Items[0].SecondParticipant.Id, u3.Id)
require.Equal(t, topDMs.Items[0].MessageCount, int64(3))
require.Equal(t, topDMs.Items[0].OutgoingMessageCount, int64(3))
require.Equal(t, topDMs.Items[1].SecondParticipant.Id, u1.Id)
require.Equal(t, topDMs.Items[1].MessageCount, int64(2))
require.Equal(t, topDMs.Items[1].OutgoingMessageCount, int64(1))
require.Equal(t, topDMs.Items[2].SecondParticipant.Id, u2.Id)
require.Equal(t, topDMs.Items[2].MessageCount, int64(1))
require.Equal(t, topDMs.Items[2].OutgoingMessageCount, int64(0))
// this also ensures that u3-u4 conversation doesn't show up in others' top DMs.
})
t.Run("topDMs should only consider user's DM channels ", func(t *testing.T) {
// u4 only takes part in one conversation
topDMs, storeErr := ss.Post().GetTopDMsForUserSince(u4.Id, 100, 0, 100)
require.NoError(t, storeErr)
// len of topDMs.Items should be 3
require.Len(t, topDMs.Items, 1)
// check order, magnitude of items
require.Equal(t, topDMs.Items[0].SecondParticipant.Id, u3.Id)
require.Equal(t, topDMs.Items[0].MessageCount, int64(4))
})
t.Run("topDMs will not consider self dms", func(t *testing.T) {
chUser, nErr := ss.Channel().CreateDirectChannel(&user, &user)
require.NoError(t, nErr)
_, err = ss.Post().Save(&model.Post{
ChannelId: chUser.Id,
UserId: user.Id,
})
// delete u2 post
err := ss.Post().Delete(postToDelete.Id, 200, user.Id)
require.NoError(t, err)
// u4 only takes part in one conversation
topDMs, err := ss.Post().GetTopDMsForUserSince(user.Id, 100, 0, 100)
require.NoError(t, err)
// len of topDMs.Items should be 3
require.Len(t, topDMs.Items, 2)
})
t.Run("topDMs will not consider deleted second user", func(t *testing.T) {
// u4 only takes part in one conversation
topDMs, err := ss.Post().GetTopDMsForUserSince(u4.Id, 100, 0, 100)
require.NoError(t, err)
// len of topDMs.Items should be 1
require.Len(t, topDMs.Items, 1)
// delete user3
err = ss.User().PermanentDelete(u3.Id)
require.NoError(t, err)
// delete user3 posts
err = ss.Post().Delete(u3u4Post1.Id, 200, u3.Id)
require.NoError(t, err)
err = ss.Post().Delete(u3u4Post2.Id, 200, u3.Id)
require.NoError(t, err)
// delete channel memberships
err = ss.Channel().PermanentDeleteMembersByUser(u3.Id)
require.NoError(t, err)
topDMs, err = ss.Post().GetTopDMsForUserSince(u4.Id, 100, 0, 100)
require.NoError(t, err)
// len of topDMs.Items should be 0 since u3 is deleted
require.Len(t, topDMs.Items, 0)
})
}
func testGetEditHistoryForPost(t *testing.T, ss store.Store) {
t.Run("should return edit history for post", func(t *testing.T) {
// create a post

View File

@ -73,7 +73,6 @@ func TestTeamStore(t *testing.T, ss store.Store) {
t.Run("GetTeamMembersForExport", func(t *testing.T) { testTeamStoreGetTeamMembersForExport(t, ss) })
t.Run("GetTeamsForUserWithPagination", func(t *testing.T) { testTeamMembersWithPagination(t, ss) })
t.Run("GroupSyncedTeamCount", func(t *testing.T) { testGroupSyncedTeamCount(t, ss) })
t.Run("GetNewTeamMembersSince", func(t *testing.T) { testGetNewTeamMembersSince(t, ss) })
}
func testTeamStoreSave(t *testing.T, ss store.Store) {
@ -3619,17 +3618,3 @@ func testGroupSyncedTeamCount(t *testing.T, ss store.Store) {
require.NoError(t, err)
require.GreaterOrEqual(t, countAfter, count+1)
}
func testGetNewTeamMembersSince(t *testing.T, ss store.Store) {
team, err := ss.Team().Save(&model.Team{
DisplayName: NewTestId(),
Name: NewTestId(),
Email: MakeEmail(),
Type: model.TeamInvite,
GroupConstrained: model.NewBool(true),
})
require.NoError(t, err)
_, _, err = ss.Team().GetNewTeamMembersSince(team.Id, 0, 0, 1000, false)
require.NoError(t, err)
}

View File

@ -27,7 +27,6 @@ func TestThreadStore(t *testing.T, ss store.Store, s SqlStore) {
t.Run("GetTeamsUnreadForUser", func(t *testing.T) { testGetTeamsUnreadForUser(t, ss) })
t.Run("GetVarious", func(t *testing.T) { testVarious(t, ss) })
t.Run("MarkAllAsReadByChannels", func(t *testing.T) { testMarkAllAsReadByChannels(t, ss) })
t.Run("GetTopThreads", func(t *testing.T) { testGetTopThreads(t, ss) })
t.Run("MarkAllAsReadByTeam", func(t *testing.T) { testMarkAllAsReadByTeam(t, ss) })
t.Run("DeleteMembershipsForChannel", func(t *testing.T) { testDeleteMembershipsForChannel(t, ss) })
}
@ -1349,347 +1348,6 @@ func testMarkAllAsReadByChannels(t *testing.T, ss store.Store) {
})
}
func testGetTopThreads(t *testing.T, ss store.Store) {
// create two users
u1 := model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
_, err := ss.User().Save(&u1)
require.NoError(t, err)
u2 := model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
_, err = ss.User().Save(&u2)
require.NoError(t, err)
u3 := model.User{
Email: MakeEmail(),
Username: model.NewId(),
}
_, err = ss.User().Save(&u3)
require.NoError(t, err)
t.Run("test get top team threads", func(t *testing.T) {
const limit = 10
team, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel, err := ss.Channel().Save(&model.Channel{
TeamId: team.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
post1, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: u1.Id,
})
require.NoError(t, err)
post2, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: u2.Id,
})
require.NoError(t, err)
threadStoreCreateReply(t, ss, channel.Id, post1.Id, post1.UserId, 2000)
threadStoreCreateReply(t, ss, channel.Id, post1.Id, post1.UserId, 2000)
threadStoreCreateReply(t, ss, channel.Id, post2.Id, post1.UserId, 2000)
// get top threads
topThreadsInTeam, err := ss.Thread().GetTopThreadsForTeamSince(team.Id, model.NewId(), 12, 0, limit)
require.NoError(t, err)
// require length of top threads to be 2
require.Len(t, topThreadsInTeam.Items, 2)
// require first element to be post1 with 2 replyCount=2
require.Equal(t, topThreadsInTeam.Items[0].PostId, post1.Id)
require.Equal(t, topThreadsInTeam.Items[0].UserId, post1.UserId)
require.Equal(t, topThreadsInTeam.Items[0].UserInformation.Id, post1.UserId)
require.Equal(t, topThreadsInTeam.Items[0].Post.ReplyCount, int64(2))
require.Equal(t, topThreadsInTeam.Items[0].Post.Message, post1.Message)
// require second element to be post2 with 2 replyCount=2
require.Equal(t, topThreadsInTeam.Items[1].PostId, post2.Id)
require.Equal(t, topThreadsInTeam.Items[1].Post.ReplyCount, int64(1))
require.Equal(t, topThreadsInTeam.Items[1].UserId, post2.UserId)
require.Equal(t, topThreadsInTeam.Items[1].UserInformation.Id, post2.UserId)
require.Equal(t, topThreadsInTeam.Items[1].Post.Message, post2.Message)
// require topThreads[i].Post is not null
require.Equal(t, topThreadsInTeam.Items[0].Post.Id, post1.Id)
require.Equal(t, topThreadsInTeam.Items[1].Post.Id, post2.Id)
})
t.Run("test get top user threads", func(t *testing.T) {
const limit = 10
team, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel, err := ss.Channel().Save(&model.Channel{
TeamId: team.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
post1, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: u1.Id,
})
require.NoError(t, err)
post2, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: u2.Id,
})
require.NoError(t, err)
post3, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: u3.Id,
})
require.NoError(t, err)
threadStoreCreateReply(t, ss, channel.Id, post1.Id, post1.UserId, 2000)
threadStoreCreateReply(t, ss, channel.Id, post1.Id, post1.UserId, 2000)
threadStoreCreateReply(t, ss, channel.Id, post2.Id, post2.UserId, 2000)
threadStoreCreateReply(t, ss, channel.Id, post2.Id, post2.UserId, 2000)
threadStoreCreateReply(t, ss, channel.Id, post3.Id, post3.UserId, 2000)
opts := store.ThreadMembershipOpts{
Following: true,
IncrementMentions: false,
UpdateFollowing: true,
UpdateViewedTimestamp: false,
UpdateParticipants: false,
}
// create threadmemberships entries.
_, err = ss.Thread().MaintainMembership(post1.UserId, post1.Id, opts)
require.NoError(t, err)
_, err = ss.Thread().MaintainMembership(post2.UserId, post2.Id, opts)
require.NoError(t, err)
_, err = ss.Thread().MaintainMembership(post2.UserId, post3.Id, opts)
require.NoError(t, err)
// get top threads by user
topThreadsByUser1, err := ss.Thread().GetTopThreadsForUserSince(team.Id, post1.UserId, 12, 0, limit)
require.NoError(t, err)
topThreadsByUser2, err := ss.Thread().GetTopThreadsForUserSince(team.Id, post2.UserId, 12, 0, limit)
require.NoError(t, err)
// require length of top threads by users to be 1,2 respectively
require.Len(t, topThreadsByUser1.Items, 1)
require.Len(t, topThreadsByUser2.Items, 2)
// require first element of topThreadsByUser1 to be post1 with 2 replyCount=2
require.Equal(t, topThreadsByUser1.Items[0].PostId, post1.Id)
require.Equal(t, topThreadsByUser1.Items[0].Post.ReplyCount, int64(2))
require.Equal(t, topThreadsByUser1.Items[0].Post.Message, post1.Message)
require.Equal(t, topThreadsByUser1.Items[0].UserId, post1.UserId)
require.Equal(t, topThreadsByUser1.Items[0].UserInformation.Id, post1.UserId)
// require elements of topThreadsByUser2 to be post2 and post3 respectively
require.Equal(t, topThreadsByUser2.Items[0].PostId, post2.Id)
require.Equal(t, topThreadsByUser2.Items[0].Post.ReplyCount, int64(2))
require.Equal(t, topThreadsByUser2.Items[0].Post.Message, post2.Message)
require.Equal(t, topThreadsByUser2.Items[0].UserId, post2.UserId)
require.Equal(t, topThreadsByUser2.Items[0].UserInformation.Id, post2.UserId)
require.Equal(t, topThreadsByUser2.Items[1].PostId, post3.Id)
require.Equal(t, topThreadsByUser2.Items[1].Post.ReplyCount, int64(1))
require.Equal(t, topThreadsByUser2.Items[1].Post.Message, post3.Message)
require.Equal(t, topThreadsByUser2.Items[1].UserId, post3.UserId)
require.Equal(t, topThreadsByUser2.Items[1].UserInformation.Id, post3.UserId)
// require topThreads[i].Post is not null
require.Equal(t, topThreadsByUser1.Items[0].Post.Id, post1.Id)
require.Equal(t, topThreadsByUser2.Items[1].Post.Id, post3.Id)
})
t.Run("test get top threads only from given teamid", func(t *testing.T) {
const limit = 10
team1, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
team2, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel1, err := ss.Channel().Save(&model.Channel{
TeamId: team1.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
channel2, err := ss.Channel().Save(&model.Channel{
TeamId: team2.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
post1, err := ss.Post().Save(&model.Post{
ChannelId: channel1.Id,
UserId: u1.Id,
})
require.NoError(t, err)
post2, err := ss.Post().Save(&model.Post{
ChannelId: channel2.Id,
UserId: u2.Id,
})
require.NoError(t, err)
threadStoreCreateReply(t, ss, channel1.Id, post1.Id, post1.UserId, 2000)
threadStoreCreateReply(t, ss, channel1.Id, post1.Id, post1.UserId, 2000)
threadStoreCreateReply(t, ss, channel2.Id, post2.Id, post2.UserId, 2000)
// assert that getting top threads from teamid 1 doesn't have post1.Id
topThreadsTeam2, err := ss.Thread().GetTopThreadsForTeamSince(team2.Id, u1.Id, 12, 0, limit)
require.NoError(t, err)
require.Len(t, topThreadsTeam2.Items, 1)
require.Equal(t, topThreadsTeam2.Items[0].Post.Id, post2.Id)
})
t.Run("test get top threads only from non-direct channels", func(t *testing.T) {
const limit = 10
team1, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel1, err := ss.Channel().CreateDirectChannel(&u1, &u2)
require.NoError(t, err)
channel2, err := ss.Channel().Save(&model.Channel{
TeamId: team1.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
post1, err := ss.Post().Save(&model.Post{
ChannelId: channel1.Id,
UserId: u1.Id,
})
require.NoError(t, err)
post2, err := ss.Post().Save(&model.Post{
ChannelId: channel2.Id,
UserId: u2.Id,
})
require.NoError(t, err)
threadStoreCreateReply(t, ss, channel1.Id, post1.Id, post1.UserId, 2000)
threadStoreCreateReply(t, ss, channel1.Id, post1.Id, post1.UserId, 2000)
threadStoreCreateReply(t, ss, channel2.Id, post2.Id, u1.Id, 2000)
opts := store.ThreadMembershipOpts{
Following: true,
IncrementMentions: false,
UpdateFollowing: true,
UpdateViewedTimestamp: false,
UpdateParticipants: false,
}
// create threadmemberships entries.
_, err = ss.Thread().MaintainMembership(u1.Id, post1.Id, opts)
require.NoError(t, err)
_, err = ss.Thread().MaintainMembership(u1.Id, post2.Id, opts)
require.NoError(t, err)
_, err = ss.Thread().MaintainMembership(u2.Id, post1.Id, opts)
require.NoError(t, err)
_, err = ss.Thread().MaintainMembership(u2.Id, post2.Id, opts)
require.NoError(t, err)
// assert that getting top threads from teamid 1 doesn't have DMs
topThreadsTeam1, err := ss.Thread().GetTopThreadsForTeamSince(team1.Id, u1.Id, 12, 0, limit)
require.NoError(t, err)
require.Len(t, topThreadsTeam1.Items, 1)
require.Equal(t, topThreadsTeam1.Items[0].Post.Id, post2.Id)
// assert that getting top threads from user 1 doesn't contain dm threads.
topUserThreads, err := ss.Thread().GetTopThreadsForUserSince(team1.Id, u1.Id, 12, 0, limit)
require.NoError(t, err)
require.Len(t, topUserThreads.Items, 1)
require.Equal(t, topUserThreads.Items[0].Post.Id, post2.Id)
})
t.Run("test get top threads doesn't exceed duration", func(t *testing.T) {
const limit = 10
team, err := ss.Team().Save(&model.Team{
DisplayName: "DisplayName",
Name: "team" + model.NewId(),
Email: MakeEmail(),
Type: model.TeamOpen,
})
require.NoError(t, err)
channel, err := ss.Channel().Save(&model.Channel{
TeamId: team.Id,
DisplayName: "DisplayName",
Name: "channel" + model.NewId(),
Type: model.ChannelTypeOpen,
}, -1)
require.NoError(t, err)
post1, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: u1.Id,
})
require.NoError(t, err)
// post 2 has replies after 10 ms unix time.
post2, err := ss.Post().Save(&model.Post{
ChannelId: channel.Id,
UserId: u2.Id,
CreateAt: 1,
})
require.NoError(t, err)
threadStoreCreateReply(t, ss, channel.Id, post1.Id, post1.UserId, 2000)
threadStoreCreateReply(t, ss, channel.Id, post1.Id, post1.UserId, 2000)
threadStoreCreateReply(t, ss, channel.Id, post2.Id, post1.UserId, 10)
// get top threads
topThreadsInTeamNewer, err := ss.Thread().GetTopThreadsForTeamSince(team.Id, model.NewId(), 12, 0, limit)
require.NoError(t, err)
// require length of top threads to be 2
require.Len(t, topThreadsInTeamNewer.Items, 1)
// require first element to be post1 with 2 replyCount=2
require.Equal(t, topThreadsInTeamNewer.Items[0].PostId, post1.Id)
// get top threads
topThreadsInTeamOlder, err := ss.Thread().GetTopThreadsForTeamSince(team.Id, model.NewId(), 9, 0, limit)
require.NoError(t, err)
// require length of top threads to be 2
require.Len(t, topThreadsInTeamOlder.Items, 2)
// require first element to be post1 with 2 replyCount=2
require.Equal(t, topThreadsInTeamOlder.Items[1].PostId, post2.Id)
})
}
func testMarkAllAsReadByTeam(t *testing.T, ss store.Store) {
createThreadMembership := func(userID, postID string) {
t.Helper()

View File

@ -1728,70 +1728,6 @@ func (s *TimerLayerChannelStore) GetTeamMembersForChannel(channelID string) ([]s
return result, err
}
func (s *TimerLayerChannelStore) GetTopChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.GetTopChannelsForTeamSince(teamID, userID, since, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetTopChannelsForTeamSince", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetTopChannelsForUserSince(userID string, teamID string, since int64, offset int, limit int) (*model.TopChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.GetTopChannelsForUserSince(userID, teamID, since, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetTopChannelsForUserSince", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetTopInactiveChannelsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.GetTopInactiveChannelsForTeamSince(teamID, userID, since, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetTopInactiveChannelsForTeamSince", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GetTopInactiveChannelsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopInactiveChannelList, error) {
start := time.Now()
result, err := s.ChannelStore.GetTopInactiveChannelsForUserSince(teamID, userID, since, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetTopInactiveChannelsForUserSince", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) GroupSyncedChannelCount() (int64, error) {
start := time.Now()
@ -2025,22 +1961,6 @@ func (s *TimerLayerChannelStore) PermanentDeleteMembersByUser(userID string) err
return err
}
func (s *TimerLayerChannelStore) PostCountsByDuration(channelIDs []string, sinceUnixMillis int64, userID *string, duration model.PostCountGrouping, groupingLocation *time.Location) ([]*model.DurationPostCount, error) {
start := time.Now()
result, err := s.ChannelStore.PostCountsByDuration(channelIDs, sinceUnixMillis, userID, duration, groupingLocation)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.PostCountsByDuration", success, elapsed)
}
return result, err
}
func (s *TimerLayerChannelStore) RemoveAllDeactivatedMembers(channelID string) error {
start := time.Now()
@ -5845,22 +5765,6 @@ func (s *TimerLayerPostStore) GetSingle(id string, inclDeleted bool) (*model.Pos
return result, err
}
func (s *TimerLayerPostStore) GetTopDMsForUserSince(userID string, since int64, offset int, limit int) (*model.TopDMList, error) {
start := time.Now()
result, err := s.PostStore.GetTopDMsForUserSince(userID, since, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetTopDMsForUserSince", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostStore) HasAutoResponsePostByUserSince(options model.GetPostsSinceOptions, userId string) (bool, error) {
start := time.Now()
@ -6644,38 +6548,6 @@ func (s *TimerLayerReactionStore) GetForPostSince(postId string, since int64, ex
return result, err
}
func (s *TimerLayerReactionStore) GetTopForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopReactionList, error) {
start := time.Now()
result, err := s.ReactionStore.GetTopForTeamSince(teamID, userID, since, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ReactionStore.GetTopForTeamSince", success, elapsed)
}
return result, err
}
func (s *TimerLayerReactionStore) GetTopForUserSince(userID string, teamID string, since int64, offset int, limit int) (*model.TopReactionList, error) {
start := time.Now()
result, err := s.ReactionStore.GetTopForUserSince(userID, teamID, since, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ReactionStore.GetTopForUserSince", success, elapsed)
}
return result, err
}
func (s *TimerLayerReactionStore) PermanentDeleteBatch(endTime int64, limit int64) (int64, error) {
start := time.Now()
@ -8675,22 +8547,6 @@ func (s *TimerLayerTeamStore) GetMembersByIds(teamID string, userIds []string, r
return result, err
}
func (s *TimerLayerTeamStore) GetNewTeamMembersSince(teamID string, since int64, offset int, limit int, showFullName bool) (*model.NewTeamMembersList, int64, error) {
start := time.Now()
result, resultVar1, err := s.TeamStore.GetNewTeamMembersSince(teamID, since, offset, limit, showFullName)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("TeamStore.GetNewTeamMembersSince", success, elapsed)
}
return result, resultVar1, err
}
func (s *TimerLayerTeamStore) GetTeamMembersForExport(userID string) ([]*model.TeamMemberForExport, error) {
start := time.Now()
@ -9378,38 +9234,6 @@ func (s *TimerLayerThreadStore) GetThreadsForUser(userId string, teamID string,
return result, err
}
func (s *TimerLayerThreadStore) GetTopThreadsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
start := time.Now()
result, err := s.ThreadStore.GetTopThreadsForTeamSince(teamID, userID, since, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.GetTopThreadsForTeamSince", success, elapsed)
}
return result, err
}
func (s *TimerLayerThreadStore) GetTopThreadsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
start := time.Now()
result, err := s.ThreadStore.GetTopThreadsForUserSince(teamID, userID, since, offset, limit)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ThreadStore.GetTopThreadsForUserSince", success, elapsed)
}
return result, err
}
func (s *TimerLayerThreadStore) GetTotalThreads(userId string, teamID string, opts model.GetUserThreadsOpts) (int64, error) {
start := time.Now()

View File

@ -134,7 +134,6 @@ func GenerateClientConfig(c *model.Config, telemetryID string, license *model.Li
props["ExperimentalSharedChannels"] = "false"
props["CollapsedThreads"] = *c.ServiceSettings.CollapsedThreads
props["EnableCustomGroups"] = "false"
props["InsightsEnabled"] = strconv.FormatBool(c.FeatureFlags.InsightsEnabled)
props["PostPriority"] = strconv.FormatBool(*c.ServiceSettings.PostPriority)
props["AllowPersistentNotifications"] = strconv.FormatBool(*c.ServiceSettings.AllowPersistentNotifications)
props["AllowPersistentNotificationsForGuests"] = strconv.FormatBool(*c.ServiceSettings.AllowPersistentNotificationsForGuests)

View File

@ -150,70 +150,6 @@ func TestGetClientConfig(t *testing.T) {
"ShowFullName": "true",
},
},
{
"Insights professional license",
&model.Config{
FeatureFlags: &model.FeatureFlags{
InsightsEnabled: true,
},
},
"",
&model.License{
Features: &model.Features{},
SkuShortName: model.LicenseShortSkuProfessional,
},
map[string]string{
"InsightsEnabled": "true",
},
},
{
"Insights enterprise license",
&model.Config{
FeatureFlags: &model.FeatureFlags{
InsightsEnabled: true,
},
},
"",
&model.License{
Features: &model.Features{},
SkuShortName: model.LicenseShortSkuEnterprise,
},
map[string]string{
"InsightsEnabled": "true",
},
},
{
"Insights other license",
&model.Config{
FeatureFlags: &model.FeatureFlags{
InsightsEnabled: true,
},
},
"",
&model.License{
Features: &model.Features{},
SkuShortName: "other",
},
map[string]string{
"InsightsEnabled": "true",
},
},
{
"Insights professional license, feature flag disabled",
&model.Config{
FeatureFlags: &model.FeatureFlags{
InsightsEnabled: false,
},
},
"",
&model.License{
Features: &model.Features{},
SkuShortName: model.LicenseShortSkuProfessional,
},
map[string]string{
"InsightsEnabled": "false",
},
},
{
"Custom groups professional license",
&model.Config{},
@ -240,11 +176,7 @@ func TestGetClientConfig(t *testing.T) {
},
{
"Custom groups other license",
&model.Config{
FeatureFlags: &model.FeatureFlags{
InsightsEnabled: true,
},
},
&model.Config{},
"",
&model.License{
Features: &model.Features{},

View File

@ -9466,10 +9466,6 @@
"id": "api.team.invite_guests_to_channels.disabled.error",
"translation": "Gastkonten sind deaktiviert"
},
{
"id": "model.insights.get_start_of_day_for_time_range.time_range.app_error",
"translation": "Ungültiger Zeitbereich."
},
{
"id": "app.collection.add_topic.exists.app_error",
"translation": "Thementyp existiert schon."

View File

@ -1977,10 +1977,6 @@
"id": "api.incoming_webhook.invalid_username.app_error",
"translation": "Invalid username."
},
{
"id": "api.insights.feature_disabled",
"translation": "Insights is behind a feature flag which is not enabled."
},
{
"id": "api.invalid_channel",
"translation": "Channel listed in the request doesn't belong to the user"
@ -5831,10 +5827,6 @@
"id": "app.insert_error",
"translation": "insert error"
},
{
"id": "app.insights.feature_disabled",
"translation": "Insights feature is disabled."
},
{
"id": "app.job.download_export_results_not_enabled",
"translation": "DownloadExportResults in config.json is false. Please set this to true to download the results of this job."
@ -6295,18 +6287,6 @@
"id": "app.post.get_root_posts.app_error",
"translation": "Unable to get the posts for the channel."
},
{
"id": "app.post.get_top_dms_for_user_since.app_error",
"translation": "Unable to get top DMs for user."
},
{
"id": "app.post.get_top_threads_for_team_since.app_error",
"translation": "Unable to get top threads for team."
},
{
"id": "app.post.get_top_threads_for_user_since.app_error",
"translation": "Unable to get top threads for user."
},
{
"id": "app.post.marshal.app_error",
"translation": "Failed to marshal post."
@ -9071,10 +9051,6 @@
"id": "model.incoming_hook.username.app_error",
"translation": "Invalid username."
},
{
"id": "model.insights.get_start_of_day_for_time_range.time_range.app_error",
"translation": "Invalid time range."
},
{
"id": "model.job.is_valid.create_at.app_error",
"translation": "Create at must be a valid time."

View File

@ -9462,10 +9462,6 @@
"id": "app.file.cloud.get.app_error",
"translation": "Unable to retrieve file: cloud storage limit exceeded."
},
{
"id": "model.insights.get_start_of_day_for_time_range.time_range.app_error",
"translation": "Invalid time range."
},
{
"id": "api.team.invite_guests_to_channels.license.error",
"translation": "Your workspace licence does not support guest accounts"

View File

@ -9467,10 +9467,6 @@
"id": "app.cloud.trial_plan_bot_message_single",
"translation": "el miembro {{.UsersNum}} del workspace {{.WorkspaceName}} ha solicitado comenzar la prueba Enterprise para acceder a: "
},
{
"id": "model.insights.get_start_of_day_for_time_range.time_range.app_error",
"translation": "Intervalo de tiempo inválido."
},
{
"id": "app.collection.add_topic.exists.app_error",
"translation": "El tipo de tema ya existe."

View File

@ -9439,10 +9439,6 @@
"id": "app.job.error",
"translation": "ジョブ実行中にエラーが発生しました。"
},
{
"id": "model.insights.get_start_of_day_for_time_range.time_range.app_error",
"translation": "時間の範囲が無効です。"
},
{
"id": "model.group.name.reserved_name.app_error",
"translation": "グループ名は予約語として既に登録されています"

View File

@ -9466,10 +9466,6 @@
"id": "api.team.invite_guests_to_channels.disabled.error",
"translation": "Gastaccounts zijn uitgeschakeld"
},
{
"id": "model.insights.get_start_of_day_for_time_range.time_range.app_error",
"translation": "Ongeldig tijdsbereik."
},
{
"id": "app.collection.add_topic.exists.app_error",
"translation": "Topictype bestaat al."

View File

@ -9467,10 +9467,6 @@
"id": "api.team.invite_guests_to_channels.disabled.error",
"translation": "Konta gości są wyłączone"
},
{
"id": "model.insights.get_start_of_day_for_time_range.time_range.app_error",
"translation": "Nieprawidłowy zakres czasu."
},
{
"id": "app.collection.add_topic.exists.app_error",
"translation": "Typ tematu już istnieje."

View File

@ -9187,10 +9187,6 @@
"id": "api.cloud.delinquency_email.missing_email_to_trigger",
"translation": "Отсутствуют обязательные поля для отправки электронного письма о просрочке."
},
{
"id": "model.insights.get_start_of_day_for_time_range.time_range.app_error",
"translation": "Неверный диапазон времени."
},
{
"id": "model.group.name.reserved_name.app_error",
"translation": "имя группы уже существует как зарезервированное имя"

View File

@ -9466,10 +9466,6 @@
"id": "api.team.invite_guests_to_channels.disabled.error",
"translation": "Gäståtkomst har inaktiverats"
},
{
"id": "model.insights.get_start_of_day_for_time_range.time_range.app_error",
"translation": "Ogiltigt tidsintervall."
},
{
"id": "app.collection.add_collection.exists.app_error",
"translation": "Samlingstypen finns redan."

View File

@ -9466,10 +9466,6 @@
"id": "api.team.invite_guests_to_channels.disabled.error",
"translation": "Konuk hesapları devre dışı bırakılmış"
},
{
"id": "model.insights.get_start_of_day_for_time_range.time_range.app_error",
"translation": "Zaman aralığı geçersiz."
},
{
"id": "app.collection.add_topic.exists.app_error",
"translation": "Konu türü zaten var."

View File

@ -9467,10 +9467,6 @@
"id": "worktemplate.category.companywide",
"translation": "全公司"
},
{
"id": "model.insights.get_start_of_day_for_time_range.time_range.app_error",
"translation": "不正确的时间范围。"
},
{
"id": "model.group.name.reserved_name.app_error",
"translation": "群组名称已作为保留名称存在"

View File

@ -3670,76 +3670,6 @@ func (c *Client4) AutocompleteChannelsForTeamForSearch(ctx context.Context, team
return ch, BuildResponse(r), nil
}
// GetTopChannelsForTeamSince will return an ordered list of the top channels in a given team.
func (c *Client4) GetTopChannelsForTeamSince(ctx context.Context, teamId string, timeRange string, page int, perPage int) (*TopChannelList, *Response, error) {
query := fmt.Sprintf("?time_range=%v&page=%v&per_page=%v", timeRange, page, perPage)
r, err := c.DoAPIGet(ctx, c.teamRoute(teamId)+"/top/channels"+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var topChannels *TopChannelList
if err := json.NewDecoder(r.Body).Decode(&topChannels); err != nil {
return nil, nil, NewAppError("GetTopChannelsForTeamSince", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return topChannels, BuildResponse(r), nil
}
// GetTopChannelsForUserSince will return an ordered list of your top channels in a given team.
func (c *Client4) GetTopChannelsForUserSince(ctx context.Context, teamId string, timeRange string, page int, perPage int) (*TopChannelList, *Response, error) {
query := fmt.Sprintf("?time_range=%v&page=%v&per_page=%v", timeRange, page, perPage)
if teamId != "" {
query += fmt.Sprintf("&team_id=%v", teamId)
}
r, err := c.DoAPIGet(ctx, c.usersRoute()+"/me/top/channels"+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var topChannels *TopChannelList
if err := json.NewDecoder(r.Body).Decode(&topChannels); err != nil {
return nil, nil, NewAppError("GetTopChannelsForUserSince", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return topChannels, BuildResponse(r), nil
}
// GetTopInactiveChannelsForTeamSince will return an ordered list of the top channels in a given team.
func (c *Client4) GetTopInactiveChannelsForTeamSince(ctx context.Context, teamId string, timeRange string, page int, perPage int) (*TopInactiveChannelList, *Response, error) {
query := fmt.Sprintf("?time_range=%v&page=%v&per_page=%v", timeRange, page, perPage)
r, err := c.DoAPIGet(ctx, c.teamRoute(teamId)+"/top/inactive_channels"+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var topInactiveChannels *TopInactiveChannelList
if jsonErr := json.NewDecoder(r.Body).Decode(&topInactiveChannels); jsonErr != nil {
return nil, nil, NewAppError("GetTopInactiveChannelsForTeamSince", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError)
}
return topInactiveChannels, BuildResponse(r), nil
}
// GetTopInactiveChannelsForUserSince will return an ordered list of your top channels in a given team.
func (c *Client4) GetTopInactiveChannelsForUserSince(ctx context.Context, teamId string, timeRange string, page int, perPage int) (*TopInactiveChannelList, *Response, error) {
query := fmt.Sprintf("?time_range=%v&page=%v&per_page=%v", timeRange, page, perPage)
if teamId != "" {
query += fmt.Sprintf("&team_id=%v", teamId)
}
r, err := c.DoAPIGet(ctx, c.usersRoute()+"/me/top/inactive_channels"+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var topInactiveChannels *TopInactiveChannelList
if jsonErr := json.NewDecoder(r.Body).Decode(&topInactiveChannels); jsonErr != nil {
return nil, nil, NewAppError("GetTopInactiveChannelsForUserSince", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError)
}
return topInactiveChannels, BuildResponse(r), nil
}
// Post Section
// CreatePost creates a post based on the provided post struct.
@ -4322,41 +4252,6 @@ func (c *Client4) DoPostActionWithCookie(ctx context.Context, postId, actionId,
return BuildResponse(r), nil
}
// GetTopThreadsForTeamSince will return an ordered list of the top channels in a given team.
func (c *Client4) GetTopThreadsForTeamSince(ctx context.Context, teamId string, timeRange string, page int, perPage int) (*TopThreadList, *Response, error) {
query := fmt.Sprintf("?time_range=%v&page=%v&per_page=%v", timeRange, page, perPage)
r, err := c.DoAPIGet(ctx, c.teamRoute(teamId)+"/top/threads"+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var topThreads *TopThreadList
if err := json.NewDecoder(r.Body).Decode(&topThreads); err != nil {
return nil, nil, NewAppError("GetTopThreadsForTeamSince", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return topThreads, BuildResponse(r), nil
}
// GetTopThreadsForUserSince will return an ordered list of your top channels in a given team.
func (c *Client4) GetTopThreadsForUserSince(ctx context.Context, teamId string, timeRange string, page int, perPage int) (*TopThreadList, *Response, error) {
query := fmt.Sprintf("?time_range=%v&page=%v&per_page=%v", timeRange, page, perPage)
if teamId != "" {
query += fmt.Sprintf("&team_id=%v", teamId)
}
r, err := c.DoAPIGet(ctx, c.usersRoute()+"/me/top/threads"+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var topThreads *TopThreadList
if err := json.NewDecoder(r.Body).Decode(&topThreads); err != nil {
return nil, nil, NewAppError("GetTopThreadsForUserSince", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return topThreads, BuildResponse(r), nil
}
// OpenInteractiveDialog sends a WebSocket event to a user's clients to
// open interactive dialogs, based on the provided trigger ID and other
// provided data. Used with interactive message buttons, menus and
@ -6849,54 +6744,6 @@ func (c *Client4) GetBulkReactions(ctx context.Context, postIds []string) (map[s
return reactions, BuildResponse(r), nil
}
func (c *Client4) GetTopReactionsForTeamSince(ctx context.Context, teamId string, timeRange string, page int, perPage int) (*TopReactionList, *Response, error) {
query := fmt.Sprintf("?time_range=%v&page=%v&per_page=%v", timeRange, page, perPage)
r, err := c.DoAPIGet(ctx, c.teamRoute(teamId)+"/top/reactions"+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var topReactions *TopReactionList
if err := json.NewDecoder(r.Body).Decode(&topReactions); err != nil {
return nil, nil, NewAppError("GetTopReactionsForTeamSince", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return topReactions, BuildResponse(r), nil
}
func (c *Client4) GetTopReactionsForUserSince(ctx context.Context, teamId string, timeRange string, page int, perPage int) (*TopReactionList, *Response, error) {
query := fmt.Sprintf("?time_range=%v&page=%v&per_page=%v", timeRange, page, perPage)
if teamId != "" {
query += fmt.Sprintf("&team_id=%v", teamId)
}
r, err := c.DoAPIGet(ctx, c.usersRoute()+"/me/top/reactions"+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var topReactions *TopReactionList
if err := json.NewDecoder(r.Body).Decode(&topReactions); err != nil {
return nil, nil, NewAppError("GetTopReactionsForUserSince", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return topReactions, BuildResponse(r), nil
}
func (c *Client4) GetTopDMsForUserSince(ctx context.Context, timeRange string, page int, perPage int) (*TopDMList, *Response, error) {
query := fmt.Sprintf("?time_range=%v&page=%v&per_page=%v", timeRange, page, perPage)
r, err := c.DoAPIGet(ctx, c.usersRoute()+"/me/top/dms"+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var topDMs *TopDMList
if jsonErr := json.NewDecoder(r.Body).Decode(&topDMs); jsonErr != nil {
return nil, nil, NewAppError("GetTopReactionsForUserSince", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError)
}
return topDMs, BuildResponse(r), nil
}
// Timezone Section
// GetSupportedTimezone returns a page of supported timezones on the system.
@ -8645,20 +8492,6 @@ func (c *Client4) GetTeamsUsage(ctx context.Context) (*TeamsUsage, *Response, er
return usage, BuildResponse(r), err
}
func (c *Client4) GetNewTeamMembersSince(ctx context.Context, teamID string, timeRange string, page int, perPage int) (*NewTeamMembersList, *Response, error) {
query := fmt.Sprintf("?time_range=%v&page=%v&per_page=%v", timeRange, page, perPage)
r, err := c.DoAPIGet(ctx, c.teamRoute(teamID)+"/top/team_members"+query, "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var newTeamMembersList *NewTeamMembersList
if jsonErr := json.NewDecoder(r.Body).Decode(&newTeamMembersList); jsonErr != nil {
return nil, nil, NewAppError("GetNewTeamMembersSince", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError)
}
return newTeamMembersList, BuildResponse(r), nil
}
func (c *Client4) SelfHostedSignupAvailable(ctx context.Context) (*Response, error) {
r, err := c.DoAPIGet(ctx, c.hostedCustomerRoute()+"/signup_available", "")

View File

@ -41,8 +41,6 @@ type FeatureFlags struct {
// Enable GraphQL feature
GraphQL bool
InsightsEnabled bool
PostPriority bool
// Enable WYSIWYG text editor
@ -71,7 +69,6 @@ func (f *FeatureFlags) SetDefaults() {
f.BoardsDataRetention = false
f.NormalizeLdapDNs = false
f.GraphQL = false
f.InsightsEnabled = false
f.CallsEnabled = true
f.PeopleProduct = false
f.DeprecateCloudFree = false

View File

@ -1,363 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"net/http"
"time"
)
type PostCountGrouping string
const (
TimeRangeToday string = "today"
TimeRange7Day string = "7_day"
TimeRange28Day string = "28_day"
PostsByHour PostCountGrouping = "hour"
PostsByDay PostCountGrouping = "day"
)
type InsightsOpts struct {
StartUnixMilli int64
Page int
PerPage int
}
type InsightsListData struct {
HasNext bool `json:"has_next"`
}
// Top Reactions
type TopReactionList struct {
InsightsListData
Items []*TopReaction `json:"items"`
}
type TopReaction struct {
EmojiName string `json:"emoji_name"`
Count int64 `json:"count"`
}
// Top Channels
type TopChannelList struct {
InsightsListData
Items []*TopChannel `json:"items"`
PostCountByDuration ChannelPostCountByDuration `json:"channel_post_counts_by_duration"`
}
func (t *TopChannelList) ChannelIDs() []string {
var ids []string
for _, item := range t.Items {
ids = append(ids, item.ID)
}
return ids
}
type TopChannel struct {
ID string `json:"id"`
Type ChannelType `json:"type"`
DisplayName string `json:"display_name"`
Name string `json:"name"`
TeamID string `json:"team_id"`
MessageCount int64 `json:"message_count"`
}
// Top Channels
type TopInactiveChannelList struct {
InsightsListData
Items []*TopInactiveChannel `json:"items"`
}
type TopInactiveChannel struct {
ID string `json:"id"`
Type ChannelType `json:"type"`
DisplayName string `json:"display_name"`
Name string `json:"name"`
LastActivityAt int64 `json:"last_activity_at"`
Participants StringArray `json:"participants"`
MessageCount int64 `json:"-"`
}
// Top Threads
type TopThreadList struct {
InsightsListData
Items []*TopThread `json:"items"`
}
type TopThread struct {
PostId string `json:"-"`
ReplyCount int64 `json:"-"`
ChannelId string `json:"channel_id"`
DisplayName string `json:"channel_display_name"`
Name string `json:"channel_name"`
Participants StringArray `json:"participants"`
UserId string `json:"-"`
UserInformation *InsightUserInformation `json:"user_information"`
Post *Post `json:"post"`
}
type InsightUserInformation struct {
Id string `json:"id"`
LastPictureUpdate int64 `json:"last_picture_update"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
NickName string `json:"nickname"`
Username string `json:"username"`
}
type TopDMInsightUserInformation struct {
InsightUserInformation
Position string `json:"position"`
}
type NewTeamMembersList struct {
InsightsListData
Items []*NewTeamMember `json:"items"`
TotalCount int64 `json:"total_count"`
}
type NewTeamMember struct {
Id string `json:"id"`
Username string `json:"username"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Position string `json:"position"`
Nickname string `json:"nickname"`
LastPictureUpdate int64 `json:"last_picture_update,omitempty"`
CreateAt int64 `json:"create_at"`
}
type DurationPostCount struct {
ChannelID string `db:"channelid"`
// Duration is an ISO8601 date string.
Duration string `db:"duration"`
PostCount int `db:"postcount"`
}
// Top DMs
type TopDM struct {
MessageCount int64 `json:"post_count"`
OutgoingMessageCount int64 `json:"outgoing_message_count"`
Participants string `json:"-"`
ChannelId string `json:"-"`
SecondParticipant *TopDMInsightUserInformation `json:"second_participant"`
}
type OutgoingMessageQueryResult struct {
ChannelId string
MessageCount int
}
type TopDMList struct {
InsightsListData
Items []*TopDM `json:"items"`
}
func TimeRangeToNumberDays(timeRange string) int {
var n int
switch timeRange {
case TimeRangeToday:
n = 1
case TimeRange7Day:
n = 7
case TimeRange28Day:
n = 28
}
return n
}
// ChannelPostCountByDuration contains a count of posts by channel id, grouped by ISO8601 date string.
// Example 1 (grouped by day):
//
// cpc := model.ChannelPostCountByDuration{
// "2009-11-11": {
// "ezbp7nqxzjgdir8riodyafr9ww": 90,
// "p949c1xdojfgzffxma3p3s3ikr": 201,
// },
// "2009-11-12": {
// "ezbp7nqxzjgdir8riodyafr9ww": 45,
// "p949c1xdojfgzffxma3p3s3ikr": 68,
// },
// }
//
// Example 2 (grouped by hour):
//
// cpc := model.ChannelPostCountByDuration{
// "2009-11-11T01": {
// "ezbp7nqxzjgdir8riodyafr9ww": 90,
// "p949c1xdojfgzffxma3p3s3ikr": 201,
// },
// "2009-11-11T02": {
// "ezbp7nqxzjgdir8riodyafr9ww": 45,
// "p949c1xdojfgzffxma3p3s3ikr": 68,
// },
// }
type ChannelPostCountByDuration map[string]map[string]int
func blankChannelCountsMap(channelIDs []string) map[string]int {
blankChannelCounts := map[string]int{}
for _, id := range channelIDs {
blankChannelCounts[id] = 0
}
return blankChannelCounts
}
func ToDailyPostCountViewModel(dpc []*DurationPostCount, startTime *time.Time, numDays int, channelIDs []string) ChannelPostCountByDuration {
viewModel := ChannelPostCountByDuration{}
keyTime := *startTime
nowAtLocation := time.Now().In(startTime.Location())
if numDays == 1 {
for keyTime.Before(nowAtLocation) {
dateTimeKey := keyTime.Format(time.RFC3339)
viewModel[dateTimeKey] = blankChannelCountsMap(channelIDs)
keyTime = keyTime.Add(time.Hour)
}
} else {
for keyTime.Before(nowAtLocation) {
dateTimeKey := keyTime.Format("2006-01-02")
viewModel[dateTimeKey] = blankChannelCountsMap(channelIDs)
keyTime = keyTime.Add(24 * time.Hour)
}
}
for _, item := range dpc {
var parseFormat string
var keyFormat string
if numDays == 1 {
parseFormat = "2006-01-02T15 "
keyFormat = time.RFC3339
} else {
parseFormat = "2006-01-02"
keyFormat = parseFormat
}
durTime, err := time.ParseInLocation(parseFormat, item.Duration, startTime.Location())
if err != nil {
continue
}
localizedKey := durTime.Format(keyFormat)
_, hasKey := viewModel[localizedKey]
if !hasKey {
viewModel[localizedKey] = map[string]int{}
}
viewModel[localizedKey][item.ChannelID] = item.PostCount
}
return viewModel
}
// Deprecated: This method doesn't perform error checking.
// Use GetStartOfDayForTimeRange instead.
//
// StartOfDayForTimeRange gets the unix start time in milliseconds from the given time range.
// Time range can be one of: "today", "7_day", or "28_day".
func StartOfDayForTimeRange(timeRange string, location *time.Location) *time.Time {
now := time.Now().In(location)
resultTime := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, location)
switch timeRange {
case TimeRange7Day:
resultTime = resultTime.Add(time.Hour * time.Duration(-144))
case TimeRange28Day:
resultTime = resultTime.Add(time.Hour * time.Duration(-648))
}
return &resultTime
}
// GetStartOfDayForTimeRange gets the unix start time in milliseconds from the given time range.
// Time range can be one of: "today", "7_day", or "28_day".
func GetStartOfDayForTimeRange(timeRange string, location *time.Location) (*time.Time, *AppError) {
now := time.Now().In(location)
resultTime := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, location)
switch timeRange {
case TimeRangeToday:
case TimeRange7Day:
resultTime = resultTime.Add(time.Hour * time.Duration(-144))
case TimeRange28Day:
resultTime = resultTime.Add(time.Hour * time.Duration(-648))
default:
return nil, NewAppError("GetStartOfDayForTimeRange", "model.insights.get_start_of_day_for_time_range.time_range.app_error", nil, "", http.StatusBadRequest)
}
return &resultTime, nil
}
// GetTopReactionListWithPagination adds a rank to each item in the given list of TopReaction and checks if there is
// another page that can be fetched based on the given limit and offset. The given list of TopReaction is assumed to be
// sorted by Count. Returns a TopReactionList.
func GetTopReactionListWithPagination(reactions []*TopReaction, limit int) *TopReactionList {
// Add pagination support
var hasNext bool
if (limit != 0) && (len(reactions) == limit+1) {
hasNext = true
reactions = reactions[:len(reactions)-1]
}
return &TopReactionList{InsightsListData: InsightsListData{HasNext: hasNext}, Items: reactions}
}
// GetTopChannelListWithPagination adds a rank to each item in the given list of TopChannel and checks if there is
// another page that can be fetched based on the given limit and offset. The given list of TopChannel is assumed to be
// sorted by Score. Returns a TopChannelList.
func GetTopChannelListWithPagination(channels []*TopChannel, limit int) *TopChannelList {
// Add pagination support
var hasNext bool
if (limit != 0) && (len(channels) == limit+1) {
hasNext = true
channels = channels[:len(channels)-1]
}
return &TopChannelList{InsightsListData: InsightsListData{HasNext: hasNext}, Items: channels}
}
// GetTopThreadListWithPagination adds a rank to each item in the given list of TopThread and checks if there is
// another page that can be fetched based on the given limit and offset. The given list of TopThread is assumed to be
// sorted by ReplyCount(score). Returns a TopThreadList.
func GetTopThreadListWithPagination(threads []*TopThread, limit int) *TopThreadList {
// Add pagination support
var hasNext bool
if (limit != 0) && (len(threads) == limit+1) {
hasNext = true
threads = threads[:len(threads)-1]
}
return &TopThreadList{InsightsListData: InsightsListData{HasNext: hasNext}, Items: threads}
}
// GetTopInactiveChannelListWithPagination adds a rank to each item in the given list of TopInactiveChannel and checks if there is
// another page that can be fetched based on the given limit and offset. The given list of TopInactiveChannel is assumed to be
// sorted by Score. Returns a TopInactiveChannelList.
func GetTopInactiveChannelListWithPagination(channels []*TopInactiveChannel, limit int) *TopInactiveChannelList {
// Add pagination support
var hasNext bool
if (limit != 0) && (len(channels) == limit+1) {
hasNext = true
channels = channels[:len(channels)-1]
}
return &TopInactiveChannelList{InsightsListData: InsightsListData{HasNext: hasNext}, Items: channels}
}
// GetTopDMListWithPagination adds a rank to each item in the given list of TopDM and checks if there is
// another page that can be fetched based on the given limit and offset. The given list of TopDM is assumed to be
// sorted by MessageCount(score). Returns a TopDMList.
func GetTopDMListWithPagination(dms []*TopDM, limit int) *TopDMList {
// Add pagination support
var hasNext bool
if (limit != 0) && (len(dms) == limit+1) {
hasNext = true
dms = dms[:len(dms)-1]
}
return &TopDMList{InsightsListData: InsightsListData{HasNext: hasNext}, Items: dms}
}
func GetNewTeamMembersListWithPagination(teamMembers []*NewTeamMember, limit int) *NewTeamMembersList {
var hasNext bool
if (limit != 0) && (len(teamMembers) == limit+1) {
hasNext = true
teamMembers = teamMembers[:len(teamMembers)-1]
}
return &NewTeamMembersList{InsightsListData: InsightsListData{HasNext: hasNext}, Items: teamMembers}
}

View File

@ -1,197 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetTopReactionListWithPagination(t *testing.T) {
reactions := []*TopReaction{
{EmojiName: "smile", Count: 200},
{EmojiName: "+1", Count: 190},
{EmojiName: "100", Count: 100},
{EmojiName: "-1", Count: 75},
{EmojiName: "checkmark", Count: 50},
{EmojiName: "mattermost", Count: 49}}
hasNextTC := []struct {
Description string
Limit int
Offset int
Expected *TopReactionList
}{
{
Description: "has one page",
Limit: len(reactions),
Offset: 0,
Expected: &TopReactionList{InsightsListData: InsightsListData{HasNext: false}, Items: reactions},
},
{
Description: "has more than one page",
Limit: len(reactions) - 1,
Offset: 0,
Expected: &TopReactionList{InsightsListData: InsightsListData{HasNext: true}, Items: reactions},
},
}
for _, test := range hasNextTC {
t.Run(test.Description, func(t *testing.T) {
actual := GetTopReactionListWithPagination(reactions, test.Limit)
assert.Equal(t, test.Expected.HasNext, actual.HasNext)
})
}
}
func TestGetTopChannelListWithPagination(t *testing.T) {
channels := []*TopChannel{
{ID: NewId(), MessageCount: 200},
{ID: NewId(), MessageCount: 150},
{ID: NewId(), MessageCount: 120},
{ID: NewId(), MessageCount: 105},
{ID: NewId(), MessageCount: 5},
{ID: NewId(), MessageCount: 2}}
hasNextTC := []struct {
Description string
Limit int
Offset int
Expected *TopChannelList
}{
{
Description: "has one page",
Limit: len(channels),
Offset: 0,
Expected: &TopChannelList{InsightsListData: InsightsListData{HasNext: false}, Items: channels},
},
{
Description: "has more than one page",
Limit: len(channels) - 1,
Offset: 0,
Expected: &TopChannelList{InsightsListData: InsightsListData{HasNext: true}, Items: channels},
},
}
for _, test := range hasNextTC {
t.Run(test.Description, func(t *testing.T) {
actual := GetTopChannelListWithPagination(channels, test.Limit)
assert.Equal(t, test.Expected.HasNext, actual.HasNext)
})
}
}
func TestGetTopThreadListWithPagination(t *testing.T) {
threads := []*TopThread{
{PostId: NewId(), ReplyCount: 100},
{PostId: NewId(), ReplyCount: 80},
{PostId: NewId(), ReplyCount: 90},
{PostId: NewId(), ReplyCount: 76},
{PostId: NewId(), ReplyCount: 43},
{PostId: NewId(), ReplyCount: 2},
{PostId: NewId(), ReplyCount: 1},
}
hasNextTT := []struct {
Description string
Limit int
Offset int
Expected *TopThreadList
}{
{
Description: "has one page",
Limit: len(threads),
Offset: 0,
Expected: &TopThreadList{InsightsListData: InsightsListData{HasNext: false}, Items: threads},
},
{
Description: "has more than one page",
Limit: len(threads) - 1,
Offset: 0,
Expected: &TopThreadList{InsightsListData: InsightsListData{HasNext: true}, Items: threads},
},
}
for _, test := range hasNextTT {
t.Run(test.Description, func(t *testing.T) {
actual := GetTopThreadListWithPagination(threads, test.Limit)
assert.Equal(t, test.Expected.HasNext, actual.HasNext)
})
}
}
func TestGetTopInactiveChannelListWithPagination(t *testing.T) {
channels := []*TopInactiveChannel{
{ID: NewId(), MessageCount: 2},
{ID: NewId(), MessageCount: 5},
{ID: NewId(), MessageCount: 7},
{ID: NewId(), MessageCount: 80},
{ID: NewId(), MessageCount: 85},
{ID: NewId(), MessageCount: 92}}
hasNextTC := []struct {
Description string
Limit int
Offset int
Expected *TopInactiveChannelList
}{
{
Description: "has one page",
Limit: len(channels),
Offset: 0,
Expected: &TopInactiveChannelList{InsightsListData: InsightsListData{HasNext: false}, Items: channels},
},
{
Description: "has more than one page",
Limit: len(channels) - 1,
Offset: 0,
Expected: &TopInactiveChannelList{InsightsListData: InsightsListData{HasNext: true}, Items: channels},
},
}
for _, test := range hasNextTC {
t.Run(test.Description, func(t *testing.T) {
actual := GetTopInactiveChannelListWithPagination(channels, test.Limit)
assert.Equal(t, test.Expected.HasNext, actual.HasNext)
})
}
}
func TestGetTopDMsListWithPagination(t *testing.T) {
dms := []*TopDM{
{SecondParticipant: &TopDMInsightUserInformation{InsightUserInformation: InsightUserInformation{Id: NewId()}}, MessageCount: 100},
{SecondParticipant: &TopDMInsightUserInformation{InsightUserInformation: InsightUserInformation{Id: NewId()}}, MessageCount: 80},
{SecondParticipant: &TopDMInsightUserInformation{InsightUserInformation: InsightUserInformation{Id: NewId()}}, MessageCount: 90},
{SecondParticipant: &TopDMInsightUserInformation{InsightUserInformation: InsightUserInformation{Id: NewId()}}, MessageCount: 76},
{SecondParticipant: &TopDMInsightUserInformation{InsightUserInformation: InsightUserInformation{Id: NewId()}}, MessageCount: 43},
{SecondParticipant: &TopDMInsightUserInformation{InsightUserInformation: InsightUserInformation{Id: NewId()}}, MessageCount: 2},
{SecondParticipant: &TopDMInsightUserInformation{InsightUserInformation: InsightUserInformation{Id: NewId()}}, MessageCount: 1},
}
hasNextTT := []struct {
Description string
Limit int
Offset int
Expected *TopDMList
}{
{
Description: "has one page",
Limit: len(dms),
Offset: 0,
Expected: &TopDMList{InsightsListData: InsightsListData{HasNext: false}, Items: dms},
},
{
Description: "has more than one page",
Limit: len(dms) - 1,
Offset: 0,
Expected: &TopDMList{InsightsListData: InsightsListData{HasNext: true}, Items: dms},
},
}
for _, test := range hasNextTT {
t.Run(test.Description, func(t *testing.T) {
actual := GetTopDMListWithPagination(dms, test.Limit)
assert.Equal(t, test.Expected.HasNext, actual.HasNext)
})
}
}

View File

@ -19,7 +19,6 @@ const (
PreferenceCategoryFlaggedPost = "flagged_post"
PreferenceCategoryFavoriteChannel = "favorite_channel"
PreferenceCategorySidebarSettings = "sidebar_settings"
PreferenceCategoryInsights = "insights"
PreferenceCategoryDisplaySettings = "display_settings"
PreferenceNameCollapsedThreadsEnabled = "collapsed_reply_threads"
@ -31,7 +30,6 @@ const (
PreferenceNameNameFormat = "name_format"
PreferenceNameUseMilitaryTime = "use_military_time"
PreferenceRecommendedNextSteps = "recommended_next_steps"
PreferenceNameInsights = "insights_tutorial_state"
PreferenceCategoryTheme = "theme"
// the name for theme props is the team id

View File

@ -133,8 +133,6 @@ export function switchToChannel(channel: Channel & {userId?: string}) {
getHistory().push(`${teamUrl}/channels/${gmChannel.name}`);
} else if (channel.type === Constants.THREADS) {
getHistory().push(`${teamUrl}/${channel.name}`);
} else if (channel.type === Constants.INSIGHTS) {
getHistory().push(`${teamUrl}/${channel.name}`);
} else {
getHistory().push(`${teamUrl}/channels/${channel.name}`);
}

View File

@ -1,921 +0,0 @@
@mixin channel-display-name {
overflow: hidden;
margin-left: 7px;
color: var(--center-channel-color);
font-family: 'Open Sans';
font-size: 12px;
font-style: normal;
font-weight: 600;
line-height: 16px;
text-overflow: ellipsis;
white-space: nowrap;
}
@mixin channel-icon {
width: 14px;
height: 14px;
color: rgba(var(--center-channel-color-rgb), 0.56);
font-size: 14px;
}
.ActivityAndInsights {
background-color: rgba(var(--center-channel-color-rgb), 0.04);
.insights-body {
padding: 24px;
overflow-x: hidden;
overflow-y: auto;
@media screen and (min-width: 1680px) {
display: flex;
flex-direction: column;
.card-row {
width: 1280px;
margin: auto;
}
}
.card-row {
&::after {
display: block;
height: 0;
clear: both;
content: " ";
}
}
@media screen and (max-width: 768px) {
padding: 16px;
}
.top-channels-card {
.Card__body {
padding-right: 12px;
@media screen and (max-width: 1019px) {
padding-left: 12px;
}
}
}
.top-reactions-card {
&.small {
@media screen and (max-width: 1279px) and (min-width: 769px) {
width: calc(100% - 16px);
}
}
.Card__header {
padding-bottom: 16px;
}
}
.top-playbooks-card,
.top-boards-card,
.least-active-channels-card,
.top-threads-card {
&.small {
@media screen and (max-width: 1279px) and (min-width: 1020px) {
width: calc((100%/2) - 16px);
}
@media screen and (max-width: 1019px) and (min-width: 769px) {
width: calc(100% - 16px);
}
}
.Card__header {
padding-bottom: 16px;
}
.Card__body {
padding: 0 12px 12px;
}
}
.top-boards-card {
.Card__body {
padding: 0 12px 18px;
}
}
.top-playbooks-card,
.least-active-channels-card {
&.medium {
@media screen and (max-width: 1279px) and (min-width: 769px) {
width: calc(100% - 16px);
}
}
}
.top-playbooks-card,
.top-dms-card {
.Card__body {
padding: 0 12px 12px;
}
}
.top-channel-container {
display: flex;
@media screen and (max-width: 1019px) {
flex-direction: column;
}
.top-channel-line-chart {
flex: 0.67;
margin-right: 16px;
@media screen and (max-width: 1019px) {
flex: 1;
padding: 12px;
margin-right: 0;
}
.col-sm-12 {
padding: 0;
.total-count {
position: relative;
.title {
display: none;
}
.content {
position: relative;
}
.chartjs-render-monitor {
width: 100% !important;
min-height: 200px !important;
max-height: 200px;
&:hover {
cursor: pointer;
}
}
}
}
}
.top-channel-list {
flex: 0.33;
margin-left: 16px;
@media screen and (max-width: 1019px) {
flex: 1;
margin-left: 0;
}
.top-channel-loading-row {
display: flex;
align-items: center;
padding: 12px 12px 12px 0;
}
.channel-row {
display: flex;
align-items: center;
padding: 10px 12px;
&:nth-of-type(1) {
span.horizontal-bar {
background-color: var(--button-bg);
}
}
&:nth-of-type(2) {
span.horizontal-bar {
background-color: var(--online-indicator);
}
}
&:nth-of-type(3) {
span.horizontal-bar {
background-color: var(--away-indicator);
}
}
&:nth-of-type(4) {
span.horizontal-bar {
background-color: var(--dnd-indicator);
}
}
&:nth-of-type(5) {
span.horizontal-bar {
background-color: var(--new-message-separator);
}
}
&:hover {
background-color: rgba(var(--center-channel-color-rgb), 0.08);
border-radius: 4px;
text-decoration: none;
}
&:focus {
text-decoration: none;
}
.channel-display-name {
display: flex;
overflow: hidden;
flex: 1 0;
i.icon {
@include channel-icon;
}
.display-name {
@include channel-display-name;
}
}
.channel-message-count {
display: flex;
flex: 1 0;
align-items: center;
.message-count {
display: inline-flex;
flex: 0.2 0;
justify-content: flex-end;
color: rgba(var(--center-channel-color-rgb), 0.72);
font-family: 'Open Sans';
font-size: 11px;
font-style: normal;
font-weight: 400;
line-height: 16px;
text-align: right;
}
span.horizontal-bar {
height: 8px;
margin-left: 16px;
border-radius: 6px;
}
}
}
}
}
.top-reaction-container {
display: flex;
align-items: center;
justify-content: center;
margin: 0 18px;
.bar-chart-entry {
display: flex;
flex: 0.2;
flex-direction: column;
align-items: center;
margin-top: auto;
&:nth-of-type(1) {
.bar-chart-data {
background-color: var(--button-bg);
}
}
&:nth-of-type(2) {
.bar-chart-data {
background-color: var(--online-indicator);
}
}
&:nth-of-type(3) {
.bar-chart-data {
background-color: var(--dnd-indicator);
}
}
&:nth-of-type(4) {
.bar-chart-data {
background-color: var(--new-message-separator);
}
}
&:nth-of-type(5) {
.bar-chart-data {
background-color: var(--away-indicator);
}
}
.reaction-count {
margin-bottom: 6px;
font-family: 'Open Sans';
font-size: 11px;
font-style: normal;
font-weight: 600;
line-height: 16px;
}
.bar-chart-data {
width: 8px;
margin-bottom: 6px;
border-radius: 6px;
}
}
}
.top-thread-container {
.top-thread-loading-container {
padding: 0 12px;
&:nth-of-type(2) {
margin-top: 20px;
}
&:nth-of-type(3) {
margin-top: 20px;
}
.top-thread-loading-row {
display: flex;
align-items: center;
padding-bottom: 8px;
}
}
.thread-list {
.thread-item {
padding: 8px 12px;
&:nth-of-type(3) {
.preview {
margin-bottom: 0;
}
}
&:hover {
background: rgba(var(--center-channel-color-rgb), 0.08);
border-radius: 4px;
cursor: pointer;
}
}
}
}
.top-board-container {
margin-bottom: -3px;
.top-board-loading-container {
display: flex;
align-items: center;
padding: 0 12px;
margin-bottom: 20px;
&:nth-of-type(4) {
margin-bottom: 4px;
}
.loading-lines {
display: flex;
flex: 1;
flex-direction: column;
margin-left: 10px;
}
}
.board-list {
.board-item {
display: flex;
align-items: center;
padding: 10px 12px;
&:hover {
background: rgba(var(--center-channel-color-rgb), 0.08);
border-radius: 4px;
cursor: pointer;
text-decoration: none;
}
&:focus {
text-decoration: none;
}
span.board-icon {
display: flex;
width: 32px;
height: 32px;
align-items: center;
justify-content: center;
float: left;
font-size: 32px;
}
.display-info {
display: flex;
overflow: hidden;
flex-direction: column;
margin-left: 12px;
.display-name {
overflow: hidden;
margin-right: 10px;
color: var(--center-channel-color);
float: left;
font-family: 'Open Sans';
font-size: 12px;
font-style: normal;
font-weight: 600;
line-height: 16px;
text-overflow: ellipsis;
white-space: nowrap;
}
.update-counts {
color: rgba(var(--center-channel-color-rgb), 0.56);
font-family: 'Open Sans';
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
}
}
.Avatars.Avatars___xs {
margin-left: auto;
float: right;
}
}
}
}
.least-active-channels-container {
.least-active-channels-loading-container {
display: flex;
width: 100%;
align-items: center;
padding: 0 12px;
margin-top: 24px;
&:nth-of-type(1) {
margin-top: 10px;
}
&:nth-of-type(4) {
margin-bottom: 10px;
}
}
.channel-list {
.channel-row {
display: flex;
align-items: center;
padding: 10px 12px;
&:hover {
background: rgba(var(--center-channel-color-rgb), 0.08);
border-radius: 4px;
cursor: pointer;
text-decoration: none;
}
&:focus {
text-decoration: none;
}
.channel-info {
display: flex;
overflow: hidden;
flex: 0.4;
flex-direction: column;
.channel-display-name {
display: flex;
overflow: hidden;
i.icon {
@include channel-icon;
}
.display-name {
@include channel-display-name;
}
}
.last-activity {
color: rgba(var(--center-channel-color-rgb), 0.56);
font-family: 'Open Sans';
font-size: 11px;
font-style: normal;
font-weight: 400;
line-height: 16px;
}
}
.Avatars {
margin-left: auto;
}
}
}
.empty-state {
margin: 38px 0;
}
}
.top-playbooks-container {
.top-playbooks-loading-container {
padding: 0 12px;
&:nth-of-type(1) {
margin-top: 12px;
}
&:nth-of-type(2) {
margin-top: 24px;
margin-bottom: 24px;
}
&:nth-of-type(3) {
margin-bottom: 12px;
> div {
&:nth-of-type(2) {
margin-bottom: 0;
}
}
}
}
.playbooks-list {
.playbook-item {
display: flex;
align-items: center;
padding: 10px 12px;
&:hover {
background: rgba(var(--center-channel-color-rgb), 0.08);
border-radius: 4px;
cursor: pointer;
text-decoration: none;
}
&:focus {
text-decoration: none;
}
&:nth-of-type(2) {
.horizontal-bar {
opacity: 0.7;
}
}
&:nth-of-type(3) {
.horizontal-bar {
opacity: 0.5;
}
}
.display-info {
display: flex;
overflow: hidden;
flex: 0.6 0;
flex-direction: column;
&.run-info {
flex: 0.4 0;
}
.display-name {
overflow: hidden;
margin-right: 10px;
color: var(--center-channel-color);
float: left;
font-family: 'Open Sans';
font-size: 12px;
font-style: normal;
font-weight: 600;
line-height: 16px;
text-overflow: ellipsis;
white-space: nowrap;
}
.last-run-time {
color: rgba(var(--center-channel-color-rgb), 0.56);
font-family: 'Open Sans';
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
}
}
.horizontal-bar {
height: 8px;
margin-top: 4px;
margin-bottom: 4px;
background-color: var(--button-bg);
border-radius: 6px;
}
}
}
.empty-state {
margin: 38px 0;
}
}
.top-dms-container {
display: flex;
align-items: center;
@media screen and (max-width: 1279px) {
flex-direction: row;
flex-wrap: wrap;
}
.dms-loading-container {
display: flex;
flex: 0.2;
flex-direction: column;
align-items: left;
margin-top: auto;
.title-line {
width: 124px;
margin-top: 16px;
margin-bottom: 8px;
}
}
.top-dms-item {
display: flex;
width: 16.66%;
flex-direction: column;
padding: 12px;
margin-top: auto;
font-family: 'Open Sans';
&:nth-of-type(1) {
margin-left: 0;
}
&:nth-of-type(6) {
margin-right: 0;
}
&:hover {
background: rgba(var(--center-channel-color-rgb), 0.08);
border-radius: 4px;
cursor: pointer;
text-decoration: none;
}
@media screen and (max-width: 1279px) {
width: 100%;
flex-direction: row;
align-items: center;
margin: 0;
&:nth-of-type(1) {
margin-top: 0;
}
}
@media screen and (max-width: 1279px) and (min-width: 880px) {
overflow: hidden;
width: 50%;
&:nth-of-type(2) {
margin-top: 0;
}
}
@media screen and (max-width: 768px) and (min-width: 576px) {
overflow: hidden;
width: 50%;
&:nth-of-type(2) {
margin-top: 0;
}
}
.Avatar-xl {
width: 72px;
min-width: 72px;
height: 72px;
}
.dm-info {
display: flex;
flex-direction: column;
margin-top: 12px;
@media screen and (max-width: 1279px) {
overflow: hidden;
width: 90%;
margin-top: 0;
margin-left: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
.dm-name {
overflow: hidden;
color: var(--center-channel-color);
font-size: 14px;
font-weight: 600;
line-height: 20px;
text-overflow: ellipsis;
white-space: nowrap;
}
.dm-role {
overflow: hidden;
min-height: 16px;
margin-top: 4px;
color: rgba(var(--center-channel-color-rgb), 0.72);
font-size: 12px;
font-weight: 400;
line-height: 16px;
text-overflow: ellipsis;
white-space: nowrap;
}
.channel-message-count {
display: flex;
align-items: center;
margin-top: 4px;
.message-count {
color: rgba(var(--center-channel-color-rgb), 0.72);
font-size: 11px;
font-weight: 400;
line-height: 16px;
}
.horizontal-bar {
min-height: 8px;
margin-left: 8px;
background-color: var(--online-indicator);
border-radius: 6px;
}
.say-hello {
margin-left: 6px;
color: var(--button-bg);
font-family: 'Open Sans';
font-size: 11px;
font-style: normal;
font-weight: 600;
line-height: 10px;
&:hover {
text-decoration: none;
}
}
}
}
}
.new-members-item {
width: 16.66%;
@media screen and (max-width: 1279px) {
width: auto;
width: 100%;
flex-direction: row;
align-items: center;
margin: 0;
&:nth-of-type(1) {
margin-top: 0;
}
}
@media screen and (max-width: 1279px) and (min-width: 1020px) {
width: 50%;
&:nth-of-type(2) {
margin-top: 0;
}
}
@media screen and (max-width: 1279px) and (min-width: 880px) {
overflow: hidden;
width: 50%;
&:nth-of-type(2) {
margin-top: 0;
}
}
@media screen and (max-width: 768px) and (min-width: 576px) {
overflow: hidden;
width: 50%;
&:nth-of-type(2) {
margin-top: 0;
}
}
&.new-members-info {
display: flex;
flex-direction: column;
align-self: flex-start;
padding: 12px;
@media screen and (max-width: 1279px) {
flex-direction: row;
}
span.total-count {
color: var(--center-channel-color);
font-family: 'Metropolis';
font-size: 52px;
font-style: normal;
font-weight: 400;
line-height: 48px;
@media screen and (max-width: 1279px) {
margin-top: 8px;
margin-left: 12px;
}
}
.members-info {
display: flex;
flex-direction: column;
@media screen and (max-width: 1279px) {
margin-top: 0;
margin-left: 24px;
}
.time-range-info {
margin-top: 2px;
color: rgba(var(--center-channel-color-rgb), 0.64);
font-family: 'Open Sans';
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
}
button.see-all-button {
display: flex;
align-items: center;
align-self: flex-start;
padding: 0;
border: none;
margin-top: 5px;
background: none;
color: var(--button-bg);
font-family: 'Open Sans';
font-size: 12px;
font-style: normal;
font-weight: 600;
line-height: 10px;
.icon {
width: 14px;
height: 14px;
font-size: 14px;
}
}
}
}
}
}
.empty-state {
display: flex;
flex: 1;
flex-direction: column;
align-items: center;
margin: 65px 0;
.empty-state-emoticon {
display: flex;
padding: 10px;
margin-bottom: 8px;
background: rgba(var(--center-channel-color-rgb), 0.08);
border-radius: 50px;
i.icon {
width: 20px;
color: rgba(var(--center-channel-color-rgb), 0.56);
font-size: 20px;
}
}
.empty-state-text {
max-width: 120px;
color: rgba(var(--center-channel-color-rgb), 0.72);
font-family: 'Open Sans';
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
text-align: center;
}
}
}
}

View File

@ -1,27 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo} from 'react';
import classNames from 'classnames';
import './activity_and_insights.scss';
import Insights from './insights/insights';
const ActivityAndInsights = () => {
/**
* Here we can do a check to see if both insights and activity are enabled. If that condition is true we can render the tabbed header.
* Otherwise we can render either insights or activity based on which flag is enabled.
*/
return (
<div
id='app-content'
className={classNames('ActivityAndInsights app__content')}
>
<Insights/>
</div>
);
};
export default memo(ActivityAndInsights);

View File

@ -1,69 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
#SidebarContainer .SidebarInsights {
.SidebarChannel {
height: 100%;
}
.SidebarLink {
width: 100%;
.icon {
padding: 3px;
margin-right: 10px;
margin-left: 1px;
i {
vertical-align: middle;
}
}
&:hover {
padding-right: 16px;
.badge {
position: initial;
visibility: visible;
}
}
}
}
// legacy
.sidebar--left .SidebarInsights {
padding-bottom: 0;
margin-top: 13px;
.icon {
width: auto;
padding-left: 1px;
margin-top: 1px;
}
.SidebarChannelLinkLabel_wrapper {
display: flex;
flex: 1;
}
.badge {
margin: 0 12px 0 0;
}
}
.tippy-box[data-placement^=right] {
&.tour-tip__box {
&.insights-tip {
.tour-tip__body {
img.insights-img {
margin-top: 2.4rem;
}
}
.tippy-arrow::before {
border-width: 0;
transform-origin: 6px 6px;
}
}
}
}

View File

@ -1,78 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {Link, useRouteMatch, useLocation, matchPath} from 'react-router-dom';
import {useIntl} from 'react-intl';
import {useDispatch, useSelector} from 'react-redux';
import classNames from 'classnames';
import {ChartLineIcon} from '@mattermost/compass-icons/components';
import {insightsAreEnabled} from 'mattermost-redux/selectors/entities/preferences';
import {getIsRhsOpen, getRhsState} from 'selectors/rhs';
import {t} from 'utils/i18n';
import {RHSStates} from 'utils/constants';
import {trackEvent} from 'actions/telemetry_actions';
import {closeRightHandSide} from 'actions/views/rhs';
import InsightsTourTip from './insights_tour_tip/insights_tour_tip';
import './activity_and_insights_link.scss';
const ActivityAndInsightsLink = () => {
const {formatMessage} = useIntl();
const dispatch = useDispatch();
const insightsEnabled = useSelector(insightsAreEnabled);
const rhsOpen = useSelector(getIsRhsOpen);
const rhsState = useSelector(getRhsState);
const {url} = useRouteMatch();
const {pathname} = useLocation();
const inInsights = matchPath(pathname, {path: '/:team/activity-and-insights'}) != null;
const openInsights = useCallback((e) => {
e.stopPropagation();
trackEvent('insights', 'sidebar_open_insights');
if (rhsOpen && rhsState === RHSStates.EDIT_HISTORY) {
dispatch(closeRightHandSide());
}
}, [rhsOpen, rhsState]);
if (!insightsEnabled) {
return null;
}
return (
<ul className='SidebarInsights NavGroupContent nav nav-pills__container'>
<li
id={'sidebar-insights-button'}
className={classNames('SidebarChannel', {
active: inInsights,
})}
tabIndex={-1}
>
<Link
onClick={openInsights}
to={`${url}/activity-and-insights`}
id='sidebarItem_insights'
draggable='false'
className={'SidebarLink sidebar-item'}
tabIndex={0}
>
<span className='icon'>
<ChartLineIcon size={14}/>
</span>
<div className='SidebarChannelLinkLabel_wrapper'>
<span className='SidebarChannelLinkLabel sidebar-item__name'>
{formatMessage({id: t('activityAndInsights.sidebarLink'), defaultMessage: 'Insights'})}
</span>
</div>
</Link>
<InsightsTourTip/>
</li>
</ul>
);
};
export default ActivityAndInsightsLink;

View File

@ -1,142 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useCallback, useEffect, useState} from 'react';
import {FormattedMessage} from 'react-intl';
import {useDispatch, useSelector} from 'react-redux';
import {useHistory} from 'react-router-dom';
import {setInsightsInitialisationState} from 'mattermost-redux/actions/preferences';
import {getBool} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentRelativeTeamUrl} from 'mattermost-redux/selectors/entities/teams';
import {GlobalState as EntitiesGlobalState} from '@mattermost/types/store';
import {Preferences} from 'mattermost-redux/constants';
import {showInsightsPulsatingDot} from 'selectors/insights';
import {GlobalState} from 'types/store';
import insightsPreview from 'images/Insights-Preview-Image.jpg';
import {TourTip, useMeasurePunchouts} from '@mattermost/components';
import {OnboardingTaskCategory, OnboardingTaskList} from 'components/onboarding_tasks';
const title = (
<FormattedMessage
id='activityAndInsights.tutorialTip.title'
defaultMessage='Introducing: Insights'
/>
);
const screen = (
<>
<FormattedMessage
id='activityAndInsights.tutorialTip.description'
defaultMessage='Check out the new Insights feature added to your workspace. See what content is most active, and learn how you and your teammates are using your workspace.'
/>
<img
src={insightsPreview}
className='insights-img'
/>
</>
);
const prevBtn = (
<FormattedMessage
id='activityAndInsights.tutorial_tip.notNow'
defaultMessage='Not now'
/>
);
const nextBtn = (
<FormattedMessage
id='activityAndInsights.tutorial_tip.viewInsights'
defaultMessage='View insights'
/>
);
const InsightsTourTip = () => {
const dispatch = useDispatch();
const history = useHistory();
const showTip = useSelector(showInsightsPulsatingDot);
const showTaskList = useSelector((state: EntitiesGlobalState) => getBool(state, OnboardingTaskCategory, OnboardingTaskList.ONBOARDING_TASK_LIST_SHOW));
const teamUrl = useSelector((state: GlobalState) => getCurrentRelativeTeamUrl(state));
const nextUrl = `${teamUrl}/activity-and-insights`;
const [tipOpened, setTipOpened] = useState(showTip);
const handleDismiss = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
dispatch(setInsightsInitialisationState({[Preferences.INSIGHTS_VIEWED]: true}));
setTipOpened(false);
}, []);
const handleNext = useCallback(() => {
dispatch(setInsightsInitialisationState({[Preferences.INSIGHTS_VIEWED]: true}));
setTipOpened(false);
history.push(nextUrl);
}, []);
const handleOpen = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
e.preventDefault();
if (tipOpened) {
dispatch(setInsightsInitialisationState({[Preferences.INSIGHTS_VIEWED]: true}));
setTipOpened(false);
} else {
setTipOpened(true);
}
}, []);
const isOnboardingOngoing = useCallback(() => {
if (showTaskList) {
return true;
}
return false;
}, [showTaskList]);
const overlayPunchOut = useMeasurePunchouts(['sidebar-insights-button'], []);
useEffect(() => {
// If the user has ongoing onboarding steps we want to just remove the insights intro modal in order to not overburden with tips
if (showTip && isOnboardingOngoing()) {
dispatch(setInsightsInitialisationState({[Preferences.INSIGHTS_VIEWED]: true}));
}
}, [showTip, isOnboardingOngoing]);
return (
<>
{
(showTip && !isOnboardingOngoing()) &&
<TourTip
show={tipOpened}
screen={screen}
title={title}
overlayPunchOut={overlayPunchOut}
placement='right-start'
pulsatingDotPlacement='right'
step={1}
singleTip={true}
showOptOut={false}
interactivePunchOut={true}
handleDismiss={handleDismiss}
handleNext={handleNext}
handleOpen={handleOpen}
handlePrevious={handleDismiss}
nextBtn={nextBtn}
prevBtn={prevBtn}
offset={[-30, 5]}
className={'insights-tip'}
/>
}
</>
);
};
export default memo(InsightsTourTip);

View File

@ -1,37 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/activity_and_insights/insights should match snapshot 1`] = `
<ContextProvider
value={
Object {
"store": Object {
"clearActions": [Function],
"dispatch": [Function],
"getActions": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
},
"subscription": Subscription {
"handleChangeWrapper": [Function],
"listeners": Object {
"notify": [Function],
},
"onStateChange": [Function],
"parentSub": undefined,
"store": Object {
"clearActions": [Function],
"dispatch": [Function],
"getActions": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
},
"unsubscribe": null,
},
}
}
>
<Memo(Insights) />
</ContextProvider>
`;

View File

@ -1,79 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/activity_and_insights/insights/insights_title should match snapshot with data 1`] = `
<Card
className="insights-card expanded top-channels-card small"
>
<CardHeader
onClick={[MockFunction]}
>
<div
className="title-and-subtitle"
>
<div
className="text-top"
>
<h2>
Top channels
</h2>
</div>
<div
className="text-bottom"
>
Top channels subtitle
</div>
</div>
<button
aria-haspopup="dialog"
aria-label="Open modal for Top channels"
className="icon"
onClick={[Function]}
>
<i
className="icon icon-chevron-right"
/>
</button>
</CardHeader>
<div
className="Card__body expanded"
>
test data
</div>
</Card>
`;
exports[`components/activity_and_insights/insights/insights_title should match snapshot with no data 1`] = `
<Card
className="insights-card expanded small"
>
<CardHeader
onClick={[MockFunction]}
>
<div
className="title-and-subtitle"
>
<div
className="text-top"
>
<h2 />
</div>
<div
className="text-bottom"
/>
</div>
<button
aria-haspopup="dialog"
aria-label="Open modal for "
className="icon"
onClick={[Function]}
>
<i
className="icon icon-chevron-right"
/>
</button>
</CardHeader>
<div
className="Card__body expanded"
/>
</Card>
`;

View File

@ -1,129 +0,0 @@
.Card.insights-card {
border: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
margin: 8px;
border-radius: 8px;
box-shadow: none;
.Card__header {
position: relative;
display: flex;
align-items: center;
padding: 24px;
&:hover {
cursor: pointer;
}
.title-and-subtitle {
display: flex;
flex-direction: column;
.text-top {
display: block;
h2 {
margin: 0;
font-size: 16px;
font-weight: 600;
line-height: 24px;
}
}
.text-bottom {
display: block;
color: rgba(var(--center-channel-color-rgb), 0.72);
font-size: 12px;
font-weight: 400;
line-height: 16px;
}
}
button.icon {
position: absolute;
top: 20px;
right: 20px;
padding: 8px;
border: none;
background: none;
border-radius: 4px;
color: rgba(var(--center-channel-color-rgb), 0.56);
&:hover {
background-color: rgba(var(--center-channel-color-rgb), 0.08);
color: rgba(var(--center-channel-color-rgb), 0.56);
}
i.icon-chevron-right {
width: 16px;
height: 16px;
font-size: 16px;
}
}
.Card__hr {
display: none;
}
}
.Card__body {
padding: 0 24px 24px;
&.expanding {
transition: none;
}
&.expanded {
overflow: visible;
margin: 0;
}
}
&:hover {
border-color: rgba(var(--center-channel-color-rgb), 0.16);
box-shadow: 0 6px 14px rgb(0 0 0 / 12%);
}
&.large {
width: auto;
clear: both;
.Card__header {
padding-bottom: 16px;
}
@media screen and (min-width: 1680px) {
width: 1264px;
align-self: center;
}
@media screen and (max-width: 1019px) {
width: calc(100% - 16px);
}
}
&.medium {
width: calc((100%/2) - 16px);
float: left;
}
&.small {
width: calc((100%/3) - 16px);
min-height: 310px;
float: left;
}
@media (max-width: 768px) {
margin: 8px 0;
&:nth-of-type(4) {
margin: 8px 0 0;
}
&.large,
&.medium,
&.small {
width: 100%;
min-height: auto;
}
}
}

View File

@ -1,44 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {shallow} from 'enzyme';
import {CardSizes} from '@mattermost/types/insights';
import InsightsCard from './card';
describe('components/activity_and_insights/insights/insights_title', () => {
const props = {
class: '',
children: <></>,
title: '',
subTitle: '',
size: CardSizes.small,
onClick: jest.fn(),
};
test('should match snapshot with no data', () => {
const wrapper = shallow(
<InsightsCard
{...props}
/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot with data', () => {
const wrapper = shallow(
<InsightsCard
{...props}
class='top-channels-card'
title='Top channels'
subTitle='Top channels subtitle'
>
{'test data'}
</InsightsCard>,
);
expect(wrapper).toMatchSnapshot();
});
});

View File

@ -1,77 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo} from 'react';
import {useIntl} from 'react-intl';
import classNames from 'classnames';
import {CardSize, CardSizes} from '@mattermost/types/insights';
import Card from 'components/card/card';
import CardHeader from 'components/card/card_header';
import './card.scss';
type Props = {
class: string;
children: React.ReactNode;
title: string;
subTitle?: string;
size: CardSize;
onClick: () => void;
}
const InsightsCard = (props: Props) => {
const {formatMessage} = useIntl();
return (
<Card
className={classNames('insights-card expanded', props.class, {
large: props.size === CardSizes.large,
medium: props.size === CardSizes.medium,
small: props.size === CardSizes.small,
})}
>
<CardHeader
onClick={props.onClick}
>
<div className='title-and-subtitle'>
<div className='text-top'>
<h2>
{props.title}
</h2>
</div>
<div className='text-bottom'>
{props.subTitle}
</div>
</div>
<button
className='icon'
onClick={(e) => {
e.stopPropagation();
props.onClick();
}}
aria-label={formatMessage(
{
id: 'insights.card.ariaLabel',
defaultMessage: 'Open modal for {title}',
},
{
title: props.title,
},
)}
aria-haspopup='dialog'
>
<i className='icon icon-chevron-right'/>
</button>
</CardHeader>
<div
className='Card__body expanded'
>
{props.children}
</div>
</Card>
);
};
export default memo(InsightsCard);

View File

@ -1,58 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {useSelector} from 'react-redux';
import {getCloudSubscription, getSubscriptionProduct} from 'mattermost-redux/selectors/entities/cloud';
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
import {isCloudLicense} from 'utils/license_utils';
import {CloudProducts, InsightsScopes} from 'utils/constants';
import {setGlobalItem} from 'actions/storage';
import {useGlobalState} from 'stores/hooks';
/**
* Returns some checks for free trial users or starter licenses
*/
export function useLicenseChecks(): {isStarterFree: boolean; isFreeTrial: boolean; isEnterpriseReady: boolean} {
const subscription = useSelector(getCloudSubscription);
const license = useSelector(getLicense);
const subscriptionProduct = useSelector(getSubscriptionProduct);
const config = useSelector(getConfig);
const isCloud = isCloudLicense(license);
const isCloudStarterFree = isCloud && subscriptionProduct?.sku === CloudProducts.STARTER;
const isCloudFreeTrial = isCloud && subscription?.is_free_trial === 'true';
const isEnterpriseReady = config.BuildEnterpriseReady === 'true';
const isSelfHostedStarter = isEnterpriseReady && (license.IsLicensed === 'false');
const isSelfHostedFreeTrial = license.IsTrial === 'true';
const isStarterFree = isCloudStarterFree || isSelfHostedStarter;
const isFreeTrial = isCloudFreeTrial || isSelfHostedFreeTrial;
return {
isStarterFree,
isFreeTrial,
isEnterpriseReady,
};
}
export function useGetFilterType(): [string, (value: string) => ReturnType<typeof setGlobalItem>] {
const {isStarterFree, isEnterpriseReady} = useLicenseChecks();
const [filterType, setFilterType] = useGlobalState(InsightsScopes.TEAM, 'insightsScope');
let returnedFilterType = filterType;
// Force MY Insights for free trial users
if (isStarterFree || !isEnterpriseReady) {
returnedFilterType = InsightsScopes.MY;
}
return [
returnedFilterType,
setFilterType,
];
}

View File

@ -1,23 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import * as redux from 'react-redux';
import mockStore from 'tests/test_store';
import Insights from './insights';
describe('components/activity_and_insights/insights', () => {
const store = mockStore({});
test('should match snapshot', () => {
const wrapper = shallow(
<redux.Provider store={store}>
<Insights/>
</redux.Provider>,
);
expect(wrapper).toMatchSnapshot();
});
});

View File

@ -1,165 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useEffect, useCallback} from 'react';
import {useDispatch, useSelector} from 'react-redux';
import {trackEvent} from 'actions/telemetry_actions';
import {suppressRHS, unsuppressRHS} from 'actions/views/rhs';
import LocalStorageStore from 'stores/local_storage_store';
import {useGlobalState} from 'stores/hooks';
import {InsightsScopes, PreviousViewedTypes, suitePluginIds} from 'utils/constants';
import {useProducts} from 'utils/products';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {selectLhsItem} from 'actions/views/lhs';
import {LhsItemType, LhsPage} from 'types/store/lhs';
import {CardSizes, InsightsWidgetTypes, TimeFrame, TimeFrames} from '@mattermost/types/insights';
import InsightsHeader from './insights_header/insights_header';
import TopChannels from './top_channels/top_channels';
import TopReactions from './top_reactions/top_reactions';
import TopThreads from './top_threads/top_threads';
import TopBoards from './top_boards/top_boards';
import LeastActiveChannels from './least_active_channels/least_active_channels';
import TopPlaybooks from './top_playbooks/top_playbooks';
import TopDMsAndNewMembers from './top_dms_and_new_members/top_dms_and_new_members';
import {useGetFilterType} from './hooks';
import './../activity_and_insights.scss';
type SelectOption = {
value: string;
label: string;
}
const Insights = () => {
const dispatch = useDispatch();
const products = useProducts();
const focalboardEnabled = false;
let playbooksEnabled = false;
if (products) {
products.forEach((product) => {
if (product.pluginId === suitePluginIds.playbooks) {
playbooksEnabled = true;
}
});
}
const currentUserId = useSelector(getCurrentUserId);
const currentTeamId = useSelector(getCurrentTeamId);
const [filterType, setFilterType] = useGetFilterType();
const [timeFrame, setTimeFrame] = useGlobalState(TimeFrames.INSIGHTS_7_DAYS as string, 'insightsTimeFrame');
const setFilterTypeTeam = useCallback(() => {
trackEvent('insights', 'change_scope_to_team_insights');
setFilterType(InsightsScopes.TEAM);
}, []);
const setFilterTypeMy = useCallback(() => {
trackEvent('insights', 'change_scope_to_my_insights');
setFilterType(InsightsScopes.MY);
}, []);
const setTimeFrameValue = useCallback((value: SelectOption) => {
setTimeFrame(value.value);
}, []);
useEffect(() => {
dispatch(selectLhsItem(LhsItemType.Page, LhsPage.Insights));
dispatch(suppressRHS);
const penultimateType = LocalStorageStore.getPreviousViewedType(currentUserId, currentTeamId);
if (penultimateType !== PreviousViewedTypes.INSIGHTS) {
LocalStorageStore.setPenultimateViewedType(currentUserId, currentTeamId, penultimateType);
LocalStorageStore.setPreviousViewedType(currentUserId, currentTeamId, PreviousViewedTypes.INSIGHTS);
}
return () => {
dispatch(unsuppressRHS);
};
}, []);
return (
<>
<InsightsHeader
filterType={filterType}
setFilterTypeTeam={setFilterTypeTeam}
setFilterTypeMy={setFilterTypeMy}
timeFrame={timeFrame}
setTimeFrame={setTimeFrameValue}
/>
<div className='insights-body'>
<TopChannels
size={CardSizes.large}
filterType={filterType}
widgetType={InsightsWidgetTypes.TOP_CHANNELS}
class={'top-channels-card'}
timeFrame={timeFrame as TimeFrame}
/>
<div className='card-row'>
<TopThreads
size={focalboardEnabled ? CardSizes.small : CardSizes.medium}
filterType={filterType}
widgetType={InsightsWidgetTypes.TOP_THREADS}
class={'top-threads-card'}
timeFrame={timeFrame as TimeFrame}
/>
{
focalboardEnabled &&
<TopBoards
size={CardSizes.small}
filterType={filterType}
widgetType={InsightsWidgetTypes.TOP_BOARDS}
class={'top-boards-card'}
timeFrame={timeFrame as TimeFrame}
/>
}
<TopReactions
size={focalboardEnabled ? CardSizes.small : CardSizes.medium}
filterType={filterType}
widgetType={InsightsWidgetTypes.TOP_REACTIONS}
class={'top-reactions-card'}
timeFrame={timeFrame as TimeFrame}
/>
</div>
<TopDMsAndNewMembers
size={CardSizes.large}
filterType={filterType}
widgetType={filterType === InsightsScopes.MY ? InsightsWidgetTypes.TOP_DMS : InsightsWidgetTypes.NEW_TEAM_MEMBERS}
class={'top-dms-card'}
timeFrame={timeFrame as TimeFrame}
/>
<div className='card-row'>
<LeastActiveChannels
size={CardSizes.medium}
filterType={filterType}
widgetType={InsightsWidgetTypes.LEAST_ACTIVE_CHANNELS}
class={'least-active-channels-card'}
timeFrame={timeFrame as TimeFrame}
/>
{
playbooksEnabled &&
<TopPlaybooks
size={CardSizes.medium}
filterType={filterType}
widgetType={InsightsWidgetTypes.TOP_PLAYBOOKS}
class={'top-playbooks-card'}
timeFrame={timeFrame as TimeFrame}
/>
}
</div>
</div>
</>
);
};
export default memo(Insights);

View File

@ -1,81 +0,0 @@
.Insights___header {
&.Header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px 12px;
border-bottom: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
background-color: var(--center-channel-bg);
grid-area: header;
.insights-title {
display: flex;
align-items: center;
padding: 2px 6px 2px 8px;
border: none;
margin: 0;
background-color: transparent;
border-radius: 4px;
color: rgba(var(--center-channel-color-rgb), 1);
font-family: 'Metropolis', sans-serif;
font-size: 16px;
font-weight: 600;
line-height: 24px;
}
button.insights-title {
i.icon {
width: 16px;
height: 16px;
&::before {
margin: 0;
}
}
&:hover {
background-color: rgba(var(--center-channel-color-rgb), 0.08);
cursor: pointer;
}
}
.MenuWrapper--open {
button.insights-title {
background-color: rgba(var(--button-bg-rgb), 0.08);
}
}
.MenuItem {
i.icon {
width: 16px;
height: 16px;
color: rgba(var(--center-channel-color-rgb), 0.56);
&::before {
margin: 0;
}
}
.RestrictedIndicator__icon-tooltip-container {
align-self: center;
padding: 7px 10px;
}
}
.react-select {
.react-select__control {
height: 32px;
min-height: 32px;
}
.react-select__value-container {
padding-left: 12px;
}
.react-select__indicators {
padding-right: 10px;
color: rgba(var(--center-channel-color-rgb), 0.64);
}
}
}
}

View File

@ -1,48 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo} from 'react';
import classNames from 'classnames';
import InsightsTitle from '../insights_title/insights_title';
import TimeFrameDropdown from '../time_frame_dropdown/time_frame_dropdown';
import './insights_header.scss';
type SelectOption = {
value: string;
label: string;
}
type Props = {
filterType: string;
setFilterTypeTeam: () => void;
setFilterTypeMy: () => void;
timeFrame: string;
setTimeFrame: (value: SelectOption) => void;
}
const InsightsHeader = (props: Props) => {
return (
<header
className={classNames('Header Insights___header')}
>
<div className='left'>
<InsightsTitle
filterType={props.filterType}
setFilterTypeTeam={props.setFilterTypeTeam}
setFilterTypeMy={props.setFilterTypeMy}
/>
</div>
<div className='right'>
<TimeFrameDropdown
timeFrame={props.timeFrame}
setTimeFrame={props.setTimeFrame}
/>
</div>
</header>
);
};
export default memo(InsightsHeader);

View File

@ -1,139 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/activity_and_insights/insights/insights_modal should match snapshot with My insights 1`] = `
<Modal
animation={true}
aria-labelledby="insightsModalLabel"
autoFocus={true}
backdrop={true}
bsClass="modal"
dialogClassName="a11y__modal insights-modal"
dialogComponentClass={[Function]}
enforceFocus={true}
id="insightsModal"
keyboard={true}
manager={
ModalManager {
"add": [Function],
"containers": Array [],
"data": Array [],
"handleContainerOverflow": true,
"hideSiblingNodes": true,
"isTopModal": [Function],
"modals": Array [],
"remove": [Function],
}
}
onExited={[MockFunction]}
onHide={[Function]}
renderBackdrop={[Function]}
restoreFocus={true}
show={true}
>
<ModalHeader
bsClass="modal-header"
closeButton={true}
closeLabel="Close"
>
<div
className="title-section"
>
<ModalTitle
bsClass="modal-title"
componentClass="h1"
id="insightsModalTitle"
>
My top reactions
</ModalTitle>
<div
className="subtitle"
>
Reactions I've used the most
</div>
</div>
<Memo(TimeFrameDropdown)
setTimeFrame={[Function]}
timeFrame="today"
/>
</ModalHeader>
<ModalBody
bsClass="modal-body"
className="overflow--visible"
componentClass="div"
>
<Memo(TopReactionsTable)
filterType="MY"
timeFrame="today"
/>
</ModalBody>
</Modal>
`;
exports[`components/activity_and_insights/insights/insights_modal should match snapshot with team 1`] = `
<Modal
animation={true}
aria-labelledby="insightsModalLabel"
autoFocus={true}
backdrop={true}
bsClass="modal"
dialogClassName="a11y__modal insights-modal"
dialogComponentClass={[Function]}
enforceFocus={true}
id="insightsModal"
keyboard={true}
manager={
ModalManager {
"add": [Function],
"containers": Array [],
"data": Array [],
"handleContainerOverflow": true,
"hideSiblingNodes": true,
"isTopModal": [Function],
"modals": Array [],
"remove": [Function],
}
}
onExited={[MockFunction]}
onHide={[Function]}
renderBackdrop={[Function]}
restoreFocus={true}
show={true}
>
<ModalHeader
bsClass="modal-header"
closeButton={true}
closeLabel="Close"
>
<div
className="title-section"
>
<ModalTitle
bsClass="modal-title"
componentClass="h1"
id="insightsModalTitle"
>
Top reactions
</ModalTitle>
<div
className="subtitle"
>
The team's most-used reactions
</div>
</div>
<Memo(TimeFrameDropdown)
setTimeFrame={[Function]}
timeFrame="7_day"
/>
</ModalHeader>
<ModalBody
bsClass="modal-body"
className="overflow--visible"
componentClass="div"
>
<Memo(TopReactionsTable)
filterType="TEAM"
timeFrame="7_day"
/>
</ModalBody>
</Modal>
`;

View File

@ -1,428 +0,0 @@
.insights-modal {
&.modal-dialog {
margin-top: calc(50vh - 340px);
}
&.join-channel-modal {
max-width: 600px;
}
.modal-content {
.modal-header {
align-items: flex-start;
padding: 22px 32px 26px;
.title-section {
display: flex;
flex: 1;
flex-direction: column;
.subtitle {
margin-top: 6px;
margin-bottom: 0;
color: rgba(var(--center-channel-color-rgb), 0.72);
font-family: 'Open Sans';
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
}
}
.react-select {
margin-right: 40px;
}
.close {
top: 16px;
}
}
.modal-body {
padding: 4px 32px 28px;
.InsightsTable {
padding: 0;
background: var(--center-channel-bg);
&.TopPlaybooksTable,
&.TopBoardsTable,
&.TopThreadsTable {
.DataGrid_row {
&:hover {
cursor: pointer;
}
}
}
&.TopDMsTable {
.DataGrid_header {
.DataGrid_cell {
&:nth-of-type(3),
&:nth-of-type(4) {
text-align: right;
}
}
}
}
&.LeastActiveChannelsTable {
.DataGrid_header {
.DataGrid_cell {
&:nth-of-type(4) {
text-align: right;
}
}
}
}
.DataGrid_header {
padding: 12px 0;
border-bottom: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
background-color: rgba(var(--center-channel-color-rgb), 0.04);
border-radius: 4px 4px 0 0;
color: var(--center-channel-color);
font-family: 'Open Sans';
font-size: 14px;
font-style: normal;
font-weight: 600;
line-height: 20px;
&:last-child {
border-bottom: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
}
.DataGrid_cell {
padding: 0 16px;
}
}
.DataGrid_rows {
background: transparent;
.DataGrid_loading {
padding: 234px 0;
}
.DataGrid_row {
display: flex;
flex-direction: row;
padding: 0;
border-bottom: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
border-left: none;
background-color: var(--center-channel-bg);
color: var(--center-channel-color);
&:hover {
border-left: none;
background-color: rgba(var(--center-channel-color-rgb), 0.08);
color: var(--center-channel-color);
}
&:nth-of-type(2) {
span.horizontal-bar {
opacity: 0.9;
}
}
&:nth-of-type(3) {
span.horizontal-bar {
opacity: 0.8;
}
}
&:nth-of-type(4) {
span.horizontal-bar {
opacity: 0.7;
}
}
&:nth-of-type(5) {
span.horizontal-bar {
opacity: 0.6;
}
}
&:nth-of-type(6) {
span.horizontal-bar {
opacity: 0.5;
}
}
&:nth-of-type(7) {
span.horizontal-bar {
opacity: 0.5;
}
}
&:nth-of-type(8) {
span.horizontal-bar {
opacity: 0.4;
}
}
&:nth-of-type(9) {
span.horizontal-bar {
opacity: 0.3;
}
}
&:nth-of-type(10) {
span.horizontal-bar {
opacity: 0.3;
}
}
.DataGrid_cell {
display: flex;
align-items: center;
padding: 0 16px;
margin: 0;
white-space: normal;
&.message-count {
justify-content: end;
}
&.rankCell {
flex: 0.2 0;
}
.emoticon {
margin: 12px 12px 12px 0;
}
span.replies,
span.cell-text {
display: inline-block;
padding: 14px 0;
font-family: 'Open Sans';
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
}
.channel-display-name {
display: flex;
overflow: hidden;
color: var(--center-channel-color);
&:hover {
text-decoration: none;
}
.icon {
color: rgba(var(--center-channel-color-rgb), 0.56);
}
span.cell-text {
overflow: hidden;
margin-left: 4px;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.times-used-container {
display: flex;
width: 100%;
align-items: center;
span.cell-text {
flex: 0.1 0;
text-align: right;
}
span.horizontal-bar {
height: 8px;
margin-left: 16px;
background: var(--button-bg);
border-radius: 6px;
&.top-dms {
background: var(--online-indicator);
opacity: 1;
}
}
}
.thread-item {
width: 100%;
padding: 12px 0;
.preview {
height: auto;
}
}
.board-item {
display: flex;
align-items: center;
span.board-icon {
display: flex;
width: 24px;
height: 24px;
align-items: center;
justify-content: center;
font-size: 24px;
}
span.board-title {
overflow: hidden;
max-width: 409px;
margin-left: 12px;
font-family: 'Open Sans';
font-size: 14px;
font-style: normal;
font-weight: 600;
line-height: 20px;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.replies,
span.board-updates,
.Avatars___xs {
margin-left: auto;
}
&.last-activity {
justify-content: flex-end;
margin-right: 8px;
.timestamp {
overflow: hidden;
color: rgba(var(--center-channel-color-rgb), 0.72);
text-overflow: ellipsis;
time {
white-space: nowrap;
}
}
}
.role {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&.actions {
overflow: visible;
}
.user-info {
display: flex;
overflow: hidden;
align-items: center;
&:hover {
text-decoration: none;
}
.display-name {
overflow: hidden;
margin-left: 12px;
color: var(--center-channel-color);
font-family: 'Open Sans';
font-size: 14px;
font-style: normal;
font-weight: 600;
line-height: 20px;
text-overflow: ellipsis;
white-space: nowrap;
}
}
time {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
}
.button-footer {
padding-top: 24px;
text-align: right;
button {
height: 40px;
border-radius: 4px;
font-size: 14px;
font-weight: 600;
&.join-channel-cancel {
padding: 13px 20px;
margin-right: 8px;
background: rgba(var(--button-bg-rgb), 0.08);
color: rgb(var(--button-bg-rgb));
line-height: 14px;
}
&.save-button {
padding: 0 20px;
}
}
}
.pagination-buttons {
display: flex;
align-items: center;
justify-content: end;
.pagination {
margin-right: 4px;
color: var(--center-channel-color);
font-family: 'Open Sans';
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
}
.pagination-button {
padding: 8px;
border: none;
background: none;
border-radius: 4px;
color: rgba(var(--center-channel-color-rgb), 0.56);
&:hover {
background-color: rgba(var(--center-channel-color-rgb), 0.08);
color: rgba(var(--center-channel-color-rgb), 0.56);
}
&:disabled {
&:hover {
background-color: transparent;
color: rgba(var(--center-channel-color-rgb), 0.56);
}
}
&:nth-of-type(1) {
margin-right: 4px;
}
i {
width: 16px;
height: 16px;
font-size: 16px;
}
}
}
}
}
}
@media (min-width: 768px) {
.insights-modal {
width: 800px;
}
}

View File

@ -1,44 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {shallow} from 'enzyme';
import {InsightsWidgetTypes, TimeFrames} from '@mattermost/types/insights';
import InsightsModal from './insights_modal';
describe('components/activity_and_insights/insights/insights_modal', () => {
const props = {
filterType: 'TEAM',
onExited: jest.fn(),
widgetType: InsightsWidgetTypes.TOP_REACTIONS,
title: 'Top reactions',
subtitle: 'The team\'s most-used reactions',
timeFrame: TimeFrames.INSIGHTS_7_DAYS,
setShowModal: jest.fn(),
};
test('should match snapshot with team', () => {
const wrapper = shallow(
<InsightsModal
{...props}
/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot with My insights', () => {
const wrapper = shallow(
<InsightsModal
{...props}
filterType={'MY'}
title={'My top reactions'}
subtitle={'Reactions I\'ve used the most'}
timeFrame={TimeFrames.INSIGHTS_1_DAY}
/>,
);
expect(wrapper).toMatchSnapshot();
});
});

View File

@ -1,155 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useState, useCallback} from 'react';
import {Modal} from 'react-bootstrap';
import {InsightsWidgetTypes, TimeFrame} from '@mattermost/types/insights';
import TimeFrameDropdown from '../time_frame_dropdown/time_frame_dropdown';
import TopReactionsTable from '../top_reactions/top_reactions_table/top_reactions_table';
import TopChannelsTable from '../top_channels/top_channels_table/top_channels_table';
import TopThreadsTable from '../top_threads/top_threads_table/top_threads_table';
import TopBoardsTable from '../top_boards/top_boards_table/top_boards_table';
import LeastActiveChannelsTable from '../least_active_channels/least_active_channels_table/least_active_channels_table';
import TopPlaybooksTable from '../top_playbooks/top_playbooks_table/top_playbooks_table';
import TopDMsTable from '../top_dms_and_new_members/top_dms_table/top_dms_table';
import NewMembersTable from '../top_dms_and_new_members/new_members_table/new_members_table';
import './../../activity_and_insights.scss';
import './insights_modal.scss';
type Props = {
onExited: () => void;
widgetType: InsightsWidgetTypes;
title: string;
subtitle: string;
filterType: string;
timeFrame: TimeFrame;
setShowModal?: (show: boolean) => void;
}
const InsightsModal = (props: Props) => {
const [show, setShow] = useState(true);
const [timeFrame, setTimeFrame] = useState(props.timeFrame);
const [offset, setOffset] = useState(0);
const setTimeFrameValue = useCallback((value) => {
setTimeFrame(value.value);
setOffset(0);
}, []);
const doHide = useCallback(() => {
props.setShowModal?.(false);
setShow(false);
}, []);
const modalContent = useCallback(() => {
switch (props.widgetType) {
case InsightsWidgetTypes.TOP_CHANNELS:
return (
<TopChannelsTable
filterType={props.filterType}
timeFrame={timeFrame}
closeModal={doHide}
/>
);
case InsightsWidgetTypes.TOP_REACTIONS:
return (
<TopReactionsTable
filterType={props.filterType}
timeFrame={timeFrame}
/>
);
case InsightsWidgetTypes.TOP_THREADS:
return (
<TopThreadsTable
filterType={props.filterType}
timeFrame={timeFrame}
closeModal={doHide}
/>
);
case InsightsWidgetTypes.TOP_BOARDS:
return (
<TopBoardsTable
filterType={props.filterType}
timeFrame={timeFrame}
closeModal={doHide}
/>
);
case InsightsWidgetTypes.LEAST_ACTIVE_CHANNELS:
return (
<LeastActiveChannelsTable
filterType={props.filterType}
timeFrame={timeFrame}
closeModal={doHide}
/>
);
case InsightsWidgetTypes.TOP_PLAYBOOKS:
return (
<TopPlaybooksTable
filterType={props.filterType}
timeFrame={timeFrame}
closeModal={doHide}
/>
);
case InsightsWidgetTypes.TOP_DMS:
return (
<TopDMsTable
filterType={props.filterType}
timeFrame={timeFrame}
closeModal={doHide}
/>
);
case InsightsWidgetTypes.NEW_TEAM_MEMBERS:
return (
<NewMembersTable
filterType={props.filterType}
timeFrame={timeFrame}
closeModal={doHide}
offset={offset}
setOffset={setOffset}
/>
);
default:
return null;
}
}, [props.widgetType, timeFrame, offset]);
return (
<Modal
dialogClassName='a11y__modal insights-modal'
show={show}
onHide={doHide}
onExited={props.onExited}
aria-labelledby='insightsModalLabel'
id='insightsModal'
>
<Modal.Header closeButton={true}>
<div className='title-section'>
<Modal.Title
componentClass='h1'
id='insightsModalTitle'
>
{props.title}
</Modal.Title>
<div className='subtitle'>
{props.subtitle}
</div>
</div>
<TimeFrameDropdown
timeFrame={timeFrame}
setTimeFrame={setTimeFrameValue}
/>
</Modal.Header>
<Modal.Body
className='overflow--visible'
>
{modalContent()}
</Modal.Body>
</Modal>
);
};
export default memo(InsightsModal);

View File

@ -1,59 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {renderWithFullContext, screen} from 'tests/react_testing_utils';
import InsightsTitle from './insights_title';
describe('components/activity_and_insights/insights/insights_title', () => {
const props = {
filterType: 'TEAM',
setFilterTypeMy: jest.fn(),
setFilterTypeTeam: jest.fn(),
};
const initialState = {
entities: {
teams: {
currentTeamId: 'team_id1',
teams: {
team_id1: {
id: 'team_id1',
name: 'team1',
},
},
},
users: {
currentUserId: 'current_user_id',
profiles: {
current_user_id: {},
},
},
},
};
test('should match snapshot with My insights', () => {
renderWithFullContext(
<InsightsTitle
{...props}
filterType={'MY'}
/>,
initialState,
);
expect(screen.getByText('My insights')).toBeInTheDocument();
});
test('should match snapshot with Team insights', () => {
renderWithFullContext(
<InsightsTitle
{...props}
/>,
initialState,
);
expect(screen.getByText('Team insights')).toBeInTheDocument();
});
});

View File

@ -1,241 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useCallback} from 'react';
import {FormattedMessage, useIntl} from 'react-intl';
import {trackEvent} from 'actions/telemetry_actions';
import MenuWrapper from 'components/widgets/menu/menu_wrapper';
import Menu from 'components/widgets/menu/menu';
import RestrictedIndicator from 'components/widgets/menu/menu_items/restricted_indicator';
import * as Utils from 'utils/utils';
import {InsightsScopes, LicenseSkus, MattermostFeatures} from 'utils/constants';
import {FREEMIUM_TO_ENTERPRISE_TRIAL_LENGTH_DAYS} from 'utils/cloud_utils';
import {useLicenseChecks} from '../hooks';
import ExternalLink from 'components/external_link';
type Props = {
filterType: string;
setFilterTypeTeam: () => void;
setFilterTypeMy: () => void;
}
const InsightsTitle = (props: Props) => {
const {formatMessage} = useIntl();
const {isStarterFree, isFreeTrial, isEnterpriseReady} = useLicenseChecks();
const title = useCallback(() => {
if (props.filterType === InsightsScopes.TEAM) {
return (
formatMessage({
id: 'insights.teamHeading',
defaultMessage: 'Team insights',
})
);
}
return (
formatMessage({
id: 'insights.myHeading',
defaultMessage: 'My insights',
})
);
}, [props.filterType]);
const openInsightsDoc = useCallback(() => {
trackEvent('insights', 'open_insights_doc');
}, []);
const openTeamInsightsDoc = useCallback(() => {
trackEvent('insights', 'open_team_insights_doc');
}, []);
const openContactSales = useCallback(() => {
trackEvent('insights', 'open_contact_sales_from_insights');
}, []);
if (!isEnterpriseReady) {
return (
<div className='insights-title'>
{title()}
</div>
);
}
return (
<MenuWrapper id='insightsFilterDropdown'>
<button className='insights-title'>
{title()}
<span className='icon'>
<i className='icon icon-chevron-down'/>
</span>
</button>
<Menu
ariaLabel={Utils.localizeMessage('insights.filter.ariaLabel', 'Insights Filter Menu')}
>
<Menu.ItemAction
id='insightsDropdownMy'
buttonClass='insights-filter-btn'
onClick={props.setFilterTypeMy}
icon={
<span className='icon'>
<i className='icon icon-account-outline'/>
</span>
}
text={Utils.localizeMessage('insights.filter.myInsights', 'My insights')}
/>
<Menu.ItemAction
id='insightsDropdownTeam'
buttonClass={'insights-filter-btn'}
onClick={props.setFilterTypeTeam}
icon={
<span className='icon'>
<i className='icon icon-account-multiple-outline'/>
</span>
}
text={Utils.localizeMessage('insights.filter.teamInsights', 'Team insights')}
disabled={isStarterFree}
sibling={(isStarterFree || isFreeTrial) && (
<RestrictedIndicator
blocked={isStarterFree}
feature={MattermostFeatures.TEAM_INSIGHTS}
minimumPlanRequiredForFeature={LicenseSkus.Professional}
tooltipMessage={formatMessage({
id: 'insights.accessModal.cloudFreeTrial',
defaultMessage: 'During your trial you are able to view Team Insights.',
})}
titleAdminPreTrial={formatMessage({
id: 'insights.accessModal.titleAdminPreTrial',
defaultMessage: 'Try team insights with a free trial',
})}
messageAdminPreTrial={formatMessage({
id: 'insights.accessModal.messageAdminPreTrial',
defaultMessage: 'Use {teamInsights} with one of our paid plans. Get the full experience of Enterprise when you start a free, {trialLength} day trial.',
},
{
trialLength: FREEMIUM_TO_ENTERPRISE_TRIAL_LENGTH_DAYS,
teamInsights: (
<ExternalLink
location='insights_title'
onClick={openTeamInsightsDoc}
href={'https://docs.mattermost.com/welcome/insights.html#team-insights'}
>
<FormattedMessage
id='insights.accessModal.teamDocsLink'
defaultMessage='Team Insights'
/>
</ExternalLink>
),
},
)}
titleAdminPostTrial={formatMessage({
id: 'insights.accessModal.titleAdminPostTrial',
defaultMessage: 'Upgrade to access team insights',
})}
messageAdminPostTrial={
formatMessage(
{
id: 'insights.accessModal.messageAdminPostTrial',
defaultMessage: 'To access your complete {insightsDoc} dashboard, including {teamInsights}, please upgrade your plan to Professional or Enterprise. For questions on upgrading your plan, please {contactSales} for support.',
},
{
insightsDoc: (
<ExternalLink
onClick={openInsightsDoc}
location='insights_title'
href={'https://docs.mattermost.com/welcome/insights.html'}
>
<FormattedMessage
id='insights.accessModal.docsLink'
defaultMessage='Insights'
/>
</ExternalLink>
),
teamInsights: (
<ExternalLink
onClick={openTeamInsightsDoc}
location='insights_title'
href={'https://docs.mattermost.com/welcome/insights.html#team-insights'}
>
<FormattedMessage
id='insights.accessModal.teamDocsLink'
defaultMessage='Team Insights'
/>
</ExternalLink>
),
contactSales: (
<ExternalLink
onClick={openContactSales}
href={'https://mattermost.com/contact-sales/'}
location='insights_title'
>
<FormattedMessage
id='insights.accessModal.contactSales'
defaultMessage='contact our Sales team'
/>
</ExternalLink>
),
},
)}
titleEndUser={formatMessage({
id: 'insights.accessModal.titleEndUser',
defaultMessage: 'Team insights are available in paid plans',
})}
messageEndUser={
formatMessage(
{
id: 'insights.accessModal.messageEndUser',
defaultMessage: 'To access your complete {insightsDoc} dashboard, including {teamInsights}, please notify your Admin to upgrade your plan to Professional or Enterprise. For questions on upgrading your plan, please {contactSales} for support.',
},
{
insightsDoc: (
<ExternalLink
onClick={openInsightsDoc}
href={'https://docs.mattermost.com/welcome/insights.html'}
location='insights_title'
>
<FormattedMessage
id='insights.accessModal.docsLink'
defaultMessage='Insights'
/>
</ExternalLink>
),
teamInsights: (
<ExternalLink
onClick={openTeamInsightsDoc}
href={'https://docs.mattermost.com/welcome/insights.html#team-insights'}
location='insights_title'
>
<FormattedMessage
id='insights.accessModal.teamDocsLink'
defaultMessage='Team Insights'
/>
</ExternalLink>
),
contactSales: (
<ExternalLink
onClick={openContactSales}
href={'https://mattermost.com/contact-sales/'}
location='insights_title'
>
<FormattedMessage
id='insights.accessModal.contactSales'
defaultMessage='contact our Sales team'
/>
</ExternalLink>
),
},
)
}
/>
)}
/>
</Menu>
</MenuWrapper>
);
};
export default memo(InsightsTitle);

View File

@ -1,140 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Provider} from 'react-redux';
import {ReactWrapper} from 'enzyme';
import {BrowserRouter} from 'react-router-dom';
import {act} from 'tests/react_testing_utils';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import mockStore from 'tests/test_store';
import {PostType} from '@mattermost/types/posts';
import JoinChannelModal from './join_channel_modal';
const actImmediate = (wrapper: ReactWrapper) =>
act(
() =>
new Promise<void>((resolve) => {
setImmediate(() => {
wrapper.update();
resolve();
});
}),
);
describe('components/activity_and_insights/insights/join_channel_modal', () => {
const props = {
thread: {
channel_id: 'channel1',
channel_display_name: 'nostrum',
channel_name: 'channel1',
participants: [
'user1',
],
user_information: {
id: 'user1',
last_picture_update: 0,
first_name: 'Kathryn',
last_name: 'Mills',
},
post: {
id: 'post1',
create_at: 1653488972484,
update_at: 1653489070820,
edit_at: 0,
delete_at: 0,
is_pinned: false,
user_id: 'user1',
channel_id: 'channel1',
root_id: '',
original_id: '',
message: 'ducimus sed aut sunt corrupti necessitatibus quasi.\nreiciendis ipsa consequuntur fugiat a eaque.',
type: '' as PostType,
props: {},
hashtags: '',
pending_post_id: '',
reply_count: 18,
last_reply_at: 0,
participants: null,
metadata: {
embeds: [],
emojis: [],
files: [],
images: {},
reactions: [],
},
},
},
onExited: jest.fn(),
currentTeamId: 'team_id1',
};
const initialState = {
entities: {
teams: {
currentTeamId: 'team_id1',
teams: {
team_id1: {
id: 'team_id1',
name: 'team1',
},
},
},
channels: {
channels: {
channel1: {
id: 'channel1',
team_id: 'team_id1',
name: 'channel1',
},
},
myMembers: {},
},
general: {
config: {},
},
users: {
currentUserId: 'current_user_id',
profiles: {
current_user_id: {
id: 'current_user_id',
},
user1: {
id: 'user1',
},
},
},
preferences: {
myPreferences: {},
},
groups: {
groups: {},
myGroups: [],
},
emojis: {
customEmoji: {},
},
},
};
test('should match snapshot', async () => {
const store = await mockStore(initialState);
const wrapper = mountWithIntl(
<Provider store={store}>
<BrowserRouter>
<JoinChannelModal
{...props}
/>
</BrowserRouter>
</Provider>,
);
await actImmediate(wrapper);
expect(wrapper.text().includes('You\'ll need to join the nostrum channel to see this thread. Do you want to join nostrum now?')).toBe(true);
});
});

View File

@ -1,111 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useState, useCallback} from 'react';
import {useDispatch, useSelector} from 'react-redux';
import {FormattedMessage} from 'react-intl';
import {Modal} from 'react-bootstrap';
import {joinChannel} from 'mattermost-redux/actions/channels';
import {TopThread} from '@mattermost/types/insights';
import {ActionResult} from 'mattermost-redux/types/actions';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/common';
import {selectPost} from 'actions/views/rhs';
import {localizeMessage} from 'utils/utils';
import SaveButton from 'components/save_button';
import './../../activity_and_insights.scss';
type Props = {
onExited: () => void;
thread: TopThread;
currentTeamId: string;
}
const JoinChannelModal = (props: Props) => {
const dispatch = useDispatch();
const [show, setShow] = useState(true);
const [saving, setSaving] = useState(false);
const currentUserId = useSelector(getCurrentUserId);
const doHide = useCallback(() => {
setShow(false);
}, []);
const openRHS = useCallback(async () => {
setSaving(true);
const {error} = await dispatch(joinChannel(currentUserId, props.currentTeamId, props.thread.channel_id, props.thread.channel_name)) as ActionResult;
if (!error) {
await dispatch(selectPost(props.thread.post));
}
doHide();
}, []);
return (
<Modal
dialogClassName='a11y__modal insights-modal join-channel-modal'
show={show}
onHide={doHide}
onExited={props.onExited}
aria-labelledby='insightsModalLabel'
id='insightsModal'
>
<Modal.Header closeButton={true}>
<div className='title-section'>
<Modal.Title
componentClass='h1'
id='insightsModalTitle'
>
<FormattedMessage
id='joinChannel.title'
defaultMessage='Join channel?'
/>
</Modal.Title>
</div>
</Modal.Header>
<Modal.Body
className='overflow--visible'
>
<FormattedMessage
id='joinChannel.desciption'
defaultMessage={'You\'ll need to join the {channel} channel to see this thread. Do you want to join {channel} now?'}
values={{
channel: <strong>{props.thread.channel_display_name}</strong>,
}}
/>
<div className='button-footer'>
<button
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
props.onExited();
}}
className='btn join-channel-cancel'
>
{localizeMessage('joinChannel.cancelButton', 'Cancel')}
</button>
<SaveButton
id='saveItems'
saving={saving}
onClick={(e) => {
e.preventDefault();
openRHS();
}}
defaultMessage={localizeMessage('joinChannel.JoinButton', 'Join')}
savingMessage={localizeMessage('joinChannel.joiningButton', 'Joining...')}
/>
</div>
</Modal.Body>
</Modal>
);
};
export default memo(JoinChannelModal);

View File

@ -1,35 +0,0 @@
.channel-action {
margin-left: auto;
button.action-wrapper {
display: flex;
width: 32px;
height: 32px;
flex-direction: column;
align-items: center;
padding: 6px 8px;
border: none;
margin-left: auto;
background: transparent;
border-radius: 4px;
color: rgba(var(--center-channel-color-rgb), 0.56);
text-decoration: none;
&:hover {
background: rgba(var(--center-channel-color-rgb), 0.08);
border-radius: 4px;
color: rgba(var(--center-channel-color-rgb), 0.72);
fill: rgba(var(--center-channel-color-rgb), 0.72);
}
&:active {
background-color: rgba(var(--button-bg-rgb), 0.08);
color: var(--button-bg);
}
i.icon {
font-size: 18px;
line-height: 18px;
}
}
}

View File

@ -1,111 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useCallback} from 'react';
import {useDispatch, useSelector} from 'react-redux';
import {useIntl} from 'react-intl';
import {trackEvent} from 'actions/telemetry_actions';
import {openModal} from 'actions/views/modals';
import {General} from 'mattermost-redux/constants';
import {leaveChannel} from 'mattermost-redux/actions/channels';
import {getCurrentRelativeTeamUrl} from 'mattermost-redux/selectors/entities/teams';
import {getMyChannelMembership} from 'mattermost-redux/selectors/entities/channels';
import {LeastActiveChannel} from '@mattermost/types/insights';
import {GlobalState} from '@mattermost/types/store';
import Constants, {ModalIdentifiers} from 'utils/constants';
import {copyToClipboard} from 'utils/utils';
import {getSiteURL} from 'utils/url';
import MenuWrapper from 'components/widgets/menu/menu_wrapper';
import Menu from 'components/widgets/menu/menu';
import LeaveChannelModal from 'components/leave_channel_modal';
import './channel_actions_menu.scss';
type Props = {
channel: LeastActiveChannel;
actionCallback?: () => Promise<void>;
}
const ChannelActionsMenu = ({channel, actionCallback}: Props) => {
const dispatch = useDispatch();
const {formatMessage} = useIntl();
const currentTeamUrl = useSelector(getCurrentRelativeTeamUrl);
const isChannelMember = useSelector((state: GlobalState) => getMyChannelMembership(state, channel.id));
const isDefault = channel.name === General.DEFAULT_CHANNEL;
const handleLeave = useCallback(async (e: Event) => {
e.preventDefault();
trackEvent('insights', 'leave_channel_action');
if (channel.type === Constants.PRIVATE_CHANNEL) {
dispatch(openModal({
modalId: ModalIdentifiers.LEAVE_PRIVATE_CHANNEL_MODAL,
dialogType: LeaveChannelModal,
dialogProps: {
channel,
callback: actionCallback,
},
}));
} else {
await dispatch(leaveChannel(channel.id));
actionCallback?.();
}
}, [channel]);
const copyLink = useCallback(() => {
trackEvent('insights', 'copy_channel_link_action');
copyToClipboard(`${getSiteURL()}${currentTeamUrl}/channels/${channel.name}`);
}, [currentTeamUrl, channel]);
return (
<div className='channel-action'>
<MenuWrapper
isDisabled={false}
stopPropagationOnToggle={true}
id={`customWrapper-${channel.id}`}
>
<button
className='icon action-wrapper'
aria-label={formatMessage({id: 'insights.leastActiveChannels.menuButtonAriaLabel', defaultMessage: 'Open manage channel menu'})}
>
<i className='icon icon-dots-vertical'/>
</button>
<Menu
openLeft={true}
openUp={false}
className={'group-actions-menu'}
ariaLabel={formatMessage({id: 'insights.leastActiveChannels.menuAriaLabel', defaultMessage: 'Manage channel menu'})}
>
<Menu.Group>
<Menu.ItemAction
onClick={handleLeave}
icon={<i className='icon-logout-variant'/>}
text={formatMessage({id: 'insights.leastActiveChannels.leaveChannel', defaultMessage: 'Leave channel'})}
disabled={false}
isDangerous={true}
show={Boolean(isChannelMember) && !isDefault}
/>
</Menu.Group>
<Menu.Group>
<Menu.ItemAction
onClick={copyLink}
icon={<i className='icon-link-variant'/>}
text={formatMessage({id: 'insights.leastActiveChannels.copyLink', defaultMessage: 'Copy link'})}
disabled={false}
/>
</Menu.Group>
</Menu>
</MenuWrapper>
</div>
);
};
export default memo(ChannelActionsMenu);

View File

@ -1,190 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Provider} from 'react-redux';
import {ReactWrapper} from 'enzyme';
import {BrowserRouter} from 'react-router-dom';
import {CardSizes, InsightsWidgetTypes, TimeFrames} from '@mattermost/types/insights';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import {act} from 'tests/react_testing_utils';
import mockStore from 'tests/test_store';
import LeastActiveChannels from './least_active_channels';
const actImmediate = (wrapper: ReactWrapper) =>
act(
() =>
new Promise<void>((resolve) => {
setImmediate(() => {
wrapper.update();
resolve();
});
}),
);
jest.mock('mattermost-redux/actions/insights', () => ({
...jest.requireActual('mattermost-redux/actions/insights'),
getMyLeastActiveChannels: () => ({type: 'adsf', data: {}}),
getLeastActiveChannelsForTeam: () => ({type: 'adsf',
data: {
has_next: false,
items: [
{
id: 'ztbmh49z7pgtbbuximwxrogxzr',
type: 'O',
display_name: 'ut',
name: 'ratione-1',
last_activity_at: 1660175452131,
participants: [],
},
{
id: 'fbgmxnxxmfy1z8m855d9m88ipe',
type: 'O',
display_name: 'veritatis',
name: 'minima-3',
last_activity_at: 1660175525869,
participants: [],
},
{
id: 'uziynciroprq3g6ohhnednoeuw',
type: 'O',
display_name: 'autem',
name: 'aut-8',
last_activity_at: 1660175775169,
participants: [],
},
{
id: 'uziynciroprq4g6ohhnednoeuw',
type: 'O',
display_name: 'dolor',
name: 'aut-9',
last_activity_at: 0,
participants: [],
},
],
}}),
}));
describe('components/activity_and_insights/insights/top_boards', () => {
const props = {
filterType: 'TEAM',
timeFrame: TimeFrames.INSIGHTS_7_DAYS,
size: CardSizes.small,
widgetType: InsightsWidgetTypes.LEAST_ACTIVE_CHANNELS,
class: 'least-active-channels-card',
timeFrameLabel: 'Last 7 days',
showModal: false,
};
const initialState = {
entities: {
teams: {
currentTeamId: 'team_id1',
teams: {
team_id1: {
id: 'team_id1',
name: 'team1',
},
},
},
channels: {
channels: {
ztbmh49z7pgtbbuximwxrogxzr: {
id: 'ztbmh49z7pgtbbuximwxrogxzr',
team_id: 'team_id1',
type: 'O',
display_name: 'ut',
name: 'ratione-1',
last_activity_at: 1660175452131,
},
fbgmxnxxmfy1z8m855d9m88ipe: {
id: 'fbgmxnxxmfy1z8m855d9m88ipe',
type: 'O',
display_name: 'veritatis',
name: 'minima-3',
last_activity_at: 1660175525869,
team_id: 'team_id1',
},
uziynciroprq3g6ohhnednoeuw: {
id: 'uziynciroprq3g6ohhnednoeuw',
type: 'O',
display_name: 'autem',
name: 'aut-8',
last_activity_at: 1660175775169,
team_id: 'team_id1',
},
uziynciroprq4g6ohhnednoeuw: {
id: 'uziynciroprq4g6ohhnednoeuw',
type: 'O',
display_name: 'dolor',
name: 'aut-9',
last_activity_at: 0,
participants: [],
},
},
myMembers: {},
},
general: {
config: {},
},
users: {
currentUserId: 'current_user_id',
profiles: {
current_user_id: {
id: 'current_user_id',
},
user1: {
id: 'user1',
},
},
},
preferences: {
myPreferences: {},
},
groups: {
groups: {},
myGroups: [],
},
emojis: {
customEmoji: {},
},
},
};
test('check if 4 channels render', async () => {
const store = await mockStore(initialState);
const wrapper = mountWithIntl(
<Provider store={store}>
<BrowserRouter>
<LeastActiveChannels
{...props}
/>
</BrowserRouter>
</Provider>,
);
await actImmediate(wrapper);
expect(wrapper.find('a.channel-row').length).toEqual(4);
});
test('check if 0 channels render', async () => {
const store = await mockStore(initialState);
const wrapper = mountWithIntl(
<Provider store={store}>
<BrowserRouter>
<LeastActiveChannels
{...props}
filterType='MY'
/>
</BrowserRouter>
</Provider>,
);
await actImmediate(wrapper);
expect(wrapper.find('.empty-state').length).toEqual(1);
});
});

View File

@ -1,127 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useState, useCallback, useEffect, useMemo} from 'react';
import {useDispatch, useSelector} from 'react-redux';
import {LeastActiveChannel} from '@mattermost/types/insights';
import {CircleSkeletonLoader, RectangleSkeletonLoader} from '@mattermost/components';
import {getMyLeastActiveChannels, getLeastActiveChannelsForTeam} from 'mattermost-redux/actions/insights';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {InsightsScopes} from 'utils/constants';
import widgetHoc, {WidgetHocProps} from '../widget_hoc/widget_hoc';
import WidgetEmptyState from '../widget_empty_state/widget_empty_state';
import LeastActiveChannelsItem from './least_active_channels_item/least_active_channels_item';
import './../../activity_and_insights.scss';
interface Props {
showModal?: boolean;
}
const LeastActiveChannels = (props: WidgetHocProps & Props) => {
const dispatch = useDispatch();
const [loading, setLoading] = useState(true);
const [leastActiveChannels, setLeastActiveChannels] = useState([] as LeastActiveChannel[]);
const currentTeamId = useSelector(getCurrentTeamId);
const getInactiveChannels = useCallback(async () => {
if (props.showModal === false) {
setLoading(true);
if (props.filterType === InsightsScopes.TEAM) {
const data: any = await dispatch(getLeastActiveChannelsForTeam(currentTeamId, 0, 3, props.timeFrame));
if (data.data?.items) {
setLeastActiveChannels(data.data.items);
}
} else {
const data: any = await dispatch(getMyLeastActiveChannels(currentTeamId, 0, 3, props.timeFrame));
if (data.data?.items) {
setLeastActiveChannels(data.data.items);
}
}
setLoading(false);
}
}, [props.timeFrame, currentTeamId, props.filterType, props.showModal]);
useEffect(() => {
getInactiveChannels();
}, [getInactiveChannels]);
const skeletonLoader = useMemo(() => {
const entries = [];
for (let i = 0; i < 4; i++) {
entries.push(
<div
className='least-active-channels-loading-container'
key={i}
>
<CircleSkeletonLoader size={16}/>
<RectangleSkeletonLoader
width='30%'
height={12}
margin='0 0 0 6px'
flex='1'
/>
<RectangleSkeletonLoader
width='30%'
height={12}
margin='0 0 0 30px'
flex='1'
/>
<RectangleSkeletonLoader
width='20%'
height={12}
margin='0 0 0 50px'
flex='1'
/>
</div>,
);
}
return entries;
}, []);
const getListItems = useCallback(() => {
return (
leastActiveChannels.map((channel, i) => {
return (
<LeastActiveChannelsItem
channel={channel}
key={i}
actionCallback={getInactiveChannels}
/>
);
})
);
}, [leastActiveChannels]);
return (
<div className='least-active-channels-container'>
{
loading &&
skeletonLoader
}
{
(leastActiveChannels && !loading) &&
<div className='channel-list'>
{getListItems()}
</div>
}
{
(leastActiveChannels.length === 0 && !loading) &&
<WidgetEmptyState
icon={'globe'}
/>
}
</div>
);
};
export default memo(widgetHoc(LeastActiveChannels));

Some files were not shown because too many files have changed in this diff Show More