Compare commits

..

75 commits

Author SHA1 Message Date
Jakobus Schürz
c4870a16d4 Merge branch '2023.09-rc' into move-mentionbutton-to-navbar 2023-10-07 19:01:07 +02:00
Hypolite Petovan
560e297274
Merge pull request #13516 from MrPetovan/2023.09-rc
Revert "Merge pull request #13512 from MrPetovan/bug/13506-allocated-width"
2023-10-07 12:48:13 -04:00
Hypolite Petovan
35e15a13fb Revert "Merge pull request #13512 from MrPetovan/bug/13506-allocated-width"
This reverts commit d1e6a16cde, reversing
changes made to 6c9d3d42d3.
2023-10-07 12:46:20 -04:00
Tobias Diekershoff
d1e6a16cde
Merge pull request #13512 from MrPetovan/bug/13506-allocated-width
Fix wrong template variable name when using content/image/single_with_height_allocation.tpl
2023-10-07 14:26:43 +02:00
Hypolite Petovan
b8ead0539f Limit width of height allocated image to parent width 2023-10-07 06:09:52 -04:00
Hypolite Petovan
182a3a4663 Fix wrong template variable name when using content/image/single_with_height_allocation.tpl 2023-10-07 06:07:07 -04:00
Michael Vogel
6c9d3d42d3
Merge pull request #13514 from MrPetovan/bug/channel-classes
User Repository\UserDefinedChannel->selectByUid instead of Factory\UserDefinedChannel->getForUser
2023-10-07 12:03:02 +02:00
Hypolite Petovan
710f9eecb2 User Repository\UserDefinedChannel->selectByUid instead of Factory\UserDefinedChannel->getForUser
- Rename Repository\Channel to Repository\UserDefinedChannel
- Add new Collection\UserDefinedChannels class
- Move Factory\Timeline->createFromTableRow to Factory\UserDefinedChannel
2023-10-07 05:48:23 -04:00
Hypolite Petovan
8ac6d0ef04
Merge pull request #13507 from xundeenergie/finalize-mention-buttons
Finalize mention buttons
2023-10-07 05:23:19 -04:00
Hypolite Petovan
ee0f8cd3c9
Merge pull request #13510 from annando/fix-thread-parent
Fix for the display of the thread parent
2023-10-07 05:22:40 -04:00
Jakobus Schürz
e63092c708 translations 2023-10-07 11:07:41 +02:00
Jakobus Schürz
2636b19171 add blanks, suggested from codereview 2023-10-07 11:06:51 +02:00
Michael
30587f09b5 Fix for the display of the thread parent 2023-10-07 08:51:47 +00:00
Michael Vogel
dd504f522f
Merge pull request #13509 from MrPetovan/bug/in-reply-to
Fix typo in "in reply to" base translation string
2023-10-07 09:34:18 +02:00
Hypolite Petovan
af3151d4db Fix typo in "in reply to" base translation string 2023-10-07 02:31:56 -04:00
Hypolite Petovan
ec9345efa6
Merge pull request #13448 from annando/user-defined-channels
Channels can now be created by users
2023-10-07 02:09:19 -04:00
Hypolite Petovan
5bc466de55
Merge pull request #13499 from xundeenergie/reference-links
add link to parent of comment
2023-10-06 23:40:13 -04:00
Jakobus Schürz
c6af5266cd add mention-/showgroup button also to profile/vcard.tpl
update translations
2023-10-07 01:28:23 +02:00
Jakobus Schürz
02b26f5969 put group-logic for "widget" to php-file 2023-10-07 01:23:29 +02:00
Jakobus Schürz
4ab03594cb Merge commit 'b3097da6f' into reference-links 2023-10-07 00:40:02 +02:00
Jakobus Schürz
fc3dad8cea fix missing blank; translations 2023-10-07 00:10:15 +02:00
Jakobus Schürz
099141e0e5 translations update 2023-10-07 00:09:58 +02:00
Jakobus Schürz
f1f92ccc29 use scrollToItem instead of own function
rename isanswerto and "Is answer to" to isreplyto and "is reply to"
2023-10-07 00:09:38 +02:00
Jakobus Schürz
3c7725183e change logic in if clause in case of codereview 2023-10-07 00:09:38 +02:00
Jakobus Schürz
f42861c58d better display of parent-link 2023-10-07 00:09:38 +02:00
Jakobus Schürz
5747518ada update translations 2023-10-07 00:09:36 +02:00
Jakobus Schürz
4796bafafe add link to parent of comment 2023-10-07 00:09:06 +02:00
Michael
b5df3cb104 Unneeded constructors removed 2023-10-06 21:08:33 +00:00
Tobias Diekershoff
b3097da6f9 update translations
DE, FR and HU translations
2023-10-06 16:13:53 +02:00
Philipp
31c8256bc6 Update messages.po 2023-10-06 16:13:52 +02:00
Michael
97fbf70fa6 Fix: The calculation of unseen circles can now be deactivated again 2023-10-06 16:13:19 +02:00
Michael
3f6480c127 Clean channel confusion 2023-10-06 11:38:29 +00:00
Michael
134cbacf31 Updates messages.po 2023-10-06 10:02:27 +00:00
Michael
36a83d13f8 Merge remote-tracking branch 'upstream/2023.09-rc' into user-defined-channels 2023-10-06 10:00:09 +00:00
Jakobus Schürz
428884c621 translations update 2023-10-06 10:28:09 +02:00
Michael
22e0bb58d9 Channel class confusion solved 2023-10-06 08:20:17 +00:00
Michael
ed24d06e0c Now there are user defined channels 2023-10-05 19:10:20 +00:00
Michael
bc3bdf3cb0 Merge remote-tracking branch 'upstream/2023.09-rc' into user-defined-channels 2023-10-05 18:19:38 +00:00
Jakobus Schürz
a82127092b use scrollToItem instead of own function
rename isanswerto and "Is answer to" to isreplyto and "is reply to"
2023-10-05 17:19:16 +02:00
Jakobus Schürz
63df9ffe2f change logic in if clause in case of codereview 2023-10-05 17:02:33 +02:00
Jakobus Schürz
eda9ea962d better display of parent-link 2023-10-05 15:36:01 +02:00
Michael
fbded95f65 Merge remote-tracking branch 'upstream/2023.09-rc' into user-defined-channels 2023-10-05 13:15:45 +00:00
Jakobus Schürz
36f8162793 update translations 2023-10-05 12:46:24 +02:00
Jakobus Schürz
268d441c48 add link to parent of comment 2023-10-05 12:39:31 +02:00
Michael
aae5f006f9 Remove unneeded functions 2023-10-05 09:25:23 +00:00
Michael
6a86eeda10 Timeline classes are split into multiple classes 2023-10-05 05:36:12 +00:00
Michael
5d6e02bef3 Some more changes after review 2023-10-05 03:17:55 +00:00
Michael
73e9190ce7 Changes after review 2023-10-05 03:16:44 +00:00
Michael
b8208974a4 Merge remote-tracking branch 'upstream/develop' into user-defined-channels 2023-10-04 09:40:32 +00:00
Michael
34521c228b Merge remote-tracking branch 'upstream/develop' into user-defined-channels 2023-10-01 08:23:05 +00:00
Michael
afb6913a2f Timelines are now an object instead of an array 2023-09-30 18:51:35 +00:00
Michael
e7d65f2d12 Merge remote-tracking branch 'upstream/develop' into user-defined-channels 2023-09-30 18:44:12 +00:00
Michael
5e1c292927 Configuration for the maximum number of post engagement entries 2023-09-26 16:00:06 +00:00
Michael
ae33a6bce1 Removed unused function 2023-09-26 05:08:42 +00:00
Michael
82684cd6cd Changes after review 2023-09-26 05:05:51 +00:00
Michael
84b38beb9a Improved link removal 2023-09-25 22:05:53 +00:00
Michael
b2758f2cdd Merge remote-tracking branch 'upstream/develop' into user-defined-channels 2023-09-25 19:39:53 +00:00
Michael
6ba26e65b8 Improved channel documentation 2023-09-25 11:19:06 +00:00
Michael
9f23bee6e4 User defined channels can now base on circles/channels 2023-09-24 00:45:07 +00:00
Michael
2164787499 Channel documentation added 2023-09-23 14:46:15 +00:00
Michael
7c6d872964 Only add images 2023-09-22 20:12:10 +00:00
Michael
aa429be4d9 Remove links from search text 2023-09-22 19:09:23 +00:00
Michael
a8a9f93e09 Special search keywords added 2023-09-22 10:28:00 +00:00
Michael
c66e841c38 Add author to the search content / fix sidebar link to channels 2023-09-22 05:16:46 +00:00
Michael
508d84b2b7 Use full text search 2023-09-21 23:27:05 +00:00
Michael
fac76a33df Improved timeline menu 2023-09-21 20:43:14 +00:00
Michael
073da9735d Channels are now added to the settings menu 2023-09-21 06:49:07 +00:00
Michael
c9f74cc55f Code standards 2023-09-20 21:42:37 +00:00
Michael
42e22ed91b Channel frontend added 2023-09-20 21:39:05 +00:00
Michael
1ec8b62b58 Merge remote-tracking branch 'upstream/develop' into user-defined-channels 2023-09-20 14:14:35 +00:00
Michael
63921ccfca Changed class names 2023-09-19 14:23:02 +00:00
Michael
5056c95d60 Show network elements in the channel widget 2023-09-19 10:20:15 +00:00
Michael
00eb6b01ff Fix selected timelines 2023-09-19 09:23:27 +00:00
Michael
a2f119e18e Fix code standards 2023-09-19 09:09:20 +00:00
Michael
d68572ea44 Channels can now be created by users 2023-09-19 09:05:28 +00:00
46 changed files with 1868 additions and 556 deletions

View file

@ -1,6 +1,6 @@
-- ------------------------------------------
-- Friendica 2023.09-rc (Giant Rhubarb)
-- DB_UPDATE_VERSION 1535
-- DB_UPDATE_VERSION 1536
-- ------------------------------------------
@ -492,6 +492,25 @@ CREATE TABLE IF NOT EXISTS `cache` (
INDEX `k_expires` (`k`,`expires`)
) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Stores temporary data';
--
-- TABLE channel
--
CREATE TABLE IF NOT EXISTS `channel` (
`id` int unsigned NOT NULL auto_increment COMMENT '',
`uid` mediumint unsigned NOT NULL COMMENT 'User id',
`label` varchar(64) NOT NULL COMMENT 'Channel label',
`description` varchar(64) COMMENT 'Channel description',
`circle` int COMMENT 'Circle or channel that this channel is based on',
`access-key` varchar(1) COMMENT 'Access key',
`include-tags` varchar(255) COMMENT 'Comma separated list of tags that will be included in the channel',
`exclude-tags` varchar(255) COMMENT 'Comma separated list of tags that aren\'t allowed in the channel',
`full-text-search` varchar(255) COMMENT 'Full text search pattern, see https://mariadb.com/kb/en/full-text-index-overview/#in-boolean-mode',
`media-type` smallint unsigned COMMENT 'Filtered media types',
PRIMARY KEY(`id`),
INDEX `uid` (`uid`),
FOREIGN KEY (`uid`) REFERENCES `user` (`uid`) ON UPDATE RESTRICT ON DELETE CASCADE
) DEFAULT COLLATE utf8mb4_general_ci COMMENT='User defined Channels';
--
-- TABLE config
--
@ -1309,6 +1328,7 @@ CREATE TABLE IF NOT EXISTS `post-engagement` (
`contact-type` tinyint NOT NULL DEFAULT 0 COMMENT 'Person, organisation, news, community, relay',
`media-type` tinyint NOT NULL DEFAULT 0 COMMENT 'Type of media in a bit array (1 = image, 2 = video, 4 = audio',
`language` varbinary(128) COMMENT 'Language information about this post',
`searchtext` mediumtext COMMENT 'Simplified text for the full text search',
`created` datetime COMMENT '',
`restricted` boolean NOT NULL DEFAULT '0' COMMENT 'If true, this post is either unlisted or not from a federated network',
`comments` mediumint unsigned COMMENT 'Number of comments',
@ -1316,6 +1336,7 @@ CREATE TABLE IF NOT EXISTS `post-engagement` (
PRIMARY KEY(`uri-id`),
INDEX `owner-id` (`owner-id`),
INDEX `created` (`created`),
FULLTEXT INDEX `searchtext` (`searchtext`),
FOREIGN KEY (`uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE,
FOREIGN KEY (`owner-id`) REFERENCES `contact` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE
) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Engagement data per post';
@ -2070,6 +2091,7 @@ CREATE VIEW `post-user-view` AS SELECT
`author`.`blocked` AS `author-blocked`,
`author`.`hidden` AS `author-hidden`,
`author`.`updated` AS `author-updated`,
`author`.`contact-type` AS `author-contact-type`,
`author`.`gsid` AS `author-gsid`,
`author`.`baseurl` AS `author-baseurl`,
`post-user`.`owner-id` AS `owner-id`,
@ -2254,6 +2276,7 @@ CREATE VIEW `post-thread-user-view` AS SELECT
`author`.`blocked` AS `author-blocked`,
`author`.`hidden` AS `author-hidden`,
`author`.`updated` AS `author-updated`,
`author`.`contact-type` AS `author-contact-type`,
`author`.`gsid` AS `author-gsid`,
`post-thread-user`.`owner-id` AS `owner-id`,
`owner`.`uri-id` AS `owner-uri-id`,
@ -2422,6 +2445,7 @@ CREATE VIEW `post-view` AS SELECT
`author`.`blocked` AS `author-blocked`,
`author`.`hidden` AS `author-hidden`,
`author`.`updated` AS `author-updated`,
`author`.`contact-type` AS `author-contact-type`,
`author`.`gsid` AS `author-gsid`,
`post`.`owner-id` AS `owner-id`,
`owner`.`uri-id` AS `owner-uri-id`,
@ -2567,6 +2591,7 @@ CREATE VIEW `post-thread-view` AS SELECT
`author`.`blocked` AS `author-blocked`,
`author`.`hidden` AS `author-hidden`,
`author`.`updated` AS `author-updated`,
`author`.`contact-type` AS `author-contact-type`,
`author`.`gsid` AS `author-gsid`,
`post-thread`.`owner-id` AS `owner-id`,
`owner`.`uri-id` AS `owner-uri-id`,

77
doc/Channels.md Normal file
View file

@ -0,0 +1,77 @@
Channels
=====
* [Home](help)
Channels are a way to discover new content or to display content that you might have missed otherwise.
There are several predefined channels, additionally you can create your own channels, based on some rules.
Channels only display posts from the last 24 hours (this value can be changed by the admin).
In the display settings in the section "Timelines" you can define which channels and other timelines you want to see in the "Channels" widget on the network page and which channels should appear in the menu bar at the top of the page.
Also in the display settings in the section "Channels" you can define all the languages that you want to see in your channels. Here you can select more than one language.
On the contact page you can define the channel frequency for every contact. The options are:
* Default frequency: Posts by this contact are displayed in the "for you" channel if you interact often with this contact or if a post reached some level of interaction.
* Display all posts of this contact: All posts from this contact will appear on the "for you" channel.
* Display only few posts: When a contact creates a lot of posts in a short period, this setting reduces the number of displayed posts in every channel.
* Never display posts: Posts from this contact will never be displayed in any channel.
Predefined Channels
---
* For you: Posts from contacts you interact with and who interact with you. In detail, it consists of:
* Posts from people you interact with on a more than average level.
* Posts from the accounts that you follow with a more than average number of interactions-
* Posts from accounts where you activated "notify on new posts" or where you have set the channel frequency accordingly.
* What's Hot: Posts with a more than average number of interactions.
* Language: Posts in your language.
* Followers: Posts from your followers that you don't follow.
* Sharers of sharers: Posts from accounts that are followed by accounts that you follow.
* Images: Posts with images.
* Audio: Posts with audio.
* Videos: Posts with videos.
User defined Channels
---
In the "Channels" settings you can create your own channels.
Each channel is defined by these values:
* Label: This value is mandatory and is used for the menu label.
* Description: A short description of the content. This can help to keep the overview, when you have got a lot of channels.
* Access Key: When you want to access this channel via an access key, you can define it here. Pay attention to not use an already used one.
* Circle: This defines the data source for this channel. By default it is set to the public timeline. There are some predefined values, like the accounts that you follow or the accounts that follow you. Also all of your circles can be selected.
* Include Tags: Comma separated list of tags. A post will be used when it contains any of the listed tags.
* Exclude Tags: Comma separated list of tags. If a post contain any of these tags, then it will not be part of nthis channel.
* Full Text Search: This can be used to include or exclude content, based on the content and some additional keywords. It uses the "boolean mode" operators from MariaDB: https://mariadb.com/kb/en/full-text-index-overview/#in-boolean-mode
* Images, Videos, Audio: When selected, you will see content with the selected media type. This can be combined. If none of these fields are checked, you will see any content, with or without attacked media.
Additional keywords for the full text search
---
Additionally to the search for content, there are additional keywords that can be used in the full text search:
* from - Use "from:nickname" or "from:nickname@domain.tld" to search for posts from a specific author.
* to - Use "from:nickname" or "from:nickname@domain.tld" to search for posts with the given contact as receiver.
* group - Use "from:nickname" or "from:nickname@domain.tld" to search for group post of the given group.
* tag - Use "tag:tagname" to search for a specific tag.
* network - Use this to include or exclude some networks from your channel.
* network:apub - ActivityPub (Used by the systems in the Fediverse)
* network:dfrn - Legacy Friendica protocol. Nowayday Friendica mostly uses ActivityPub.
* network:dspr - The Diaspora protocol is mainly used by Diaspora itself. Some other systems support the protocol as well like Hubzilla, Socialhome or Ganggo.
* network:feed - RSS/Atom feeds
* network:mail - Mails that had been imported via IMAP.
* network:stat - The OStatus protocol is mainly used by old GNU Social installations.
* network:dscs - Posts that are received by the Discourse connector.
* network:tmbl - Posts that are received by the Tumblr connector.
* network:bsky - Posts that are received by the Bluesky connector.
* visibility - You have the choice between different visibilities. You can only see unlisted or private posts that you have the access for.
* visibility:public
* visibility:unlisted
* visibility:private
Remember that you can combine these kerywords.
So for example you can create a channel with all posts that talk about the Fediverse - that aren't posted in the Fediverse with the search terms: "fediverse -network:apub -network:dfrn"

View file

@ -17,6 +17,7 @@ Friendica Documentation and Resources
* [Circles and Privacy](help/Circles-and-Privacy)
* [Tags and Mentions](help/Tags-and-Mentions)
* [Community Groups](help/Groups)
* [Channels](help/Channels)
* [Chats](help/Chats)
* Further information
* [Move your account](help/Move-Account)

View file

@ -17,6 +17,7 @@ Database Tables
| [arrived-activity](help/database/db_arrived-activity) | Id of arrived activities |
| [attach](help/database/db_attach) | file attachments |
| [cache](help/database/db_cache) | Stores temporary data |
| [channel](help/database/db_channel) | User defined Channels |
| [config](help/database/db_config) | main configuration storage |
| [contact](help/database/db_contact) | contact table |
| [contact-relation](help/database/db_contact-relation) | Contact relations |

View file

@ -0,0 +1,37 @@
Table channel
===========
User defined Channels
Fields
------
| Field | Description | Type | Null | Key | Default | Extra |
| ---------------- | ------------------------------------------------------------------------------------------------- | ------------------ | ---- | --- | ------- | -------------- |
| id | | int unsigned | NO | PRI | NULL | auto_increment |
| uid | User id | mediumint unsigned | NO | | NULL | |
| label | Channel label | varchar(64) | NO | | NULL | |
| description | Channel description | varchar(64) | YES | | NULL | |
| circle | Circle or channel that this channel is based on | int | YES | | NULL | |
| access-key | Access key | varchar(1) | YES | | NULL | |
| include-tags | Comma separated list of tags that will be included in the channel | varchar(255) | YES | | NULL | |
| exclude-tags | Comma separated list of tags that aren't allowed in the channel | varchar(255) | YES | | NULL | |
| full-text-search | Full text search pattern, see https://mariadb.com/kb/en/full-text-index-overview/#in-boolean-mode | varchar(255) | YES | | NULL | |
| media-type | Filtered media types | smallint unsigned | YES | | NULL | |
Indexes
------------
| Name | Fields |
| ------- | ------ |
| PRIMARY | id |
| uid | uid |
Foreign Keys
------------
| Field | Target Table | Target Field |
|-------|--------------|--------------|
| uid | [user](help/database/db_user) | uid |
Return to [database documentation](help/database)

View file

@ -13,6 +13,7 @@ Fields
| contact-type | Person, organisation, news, community, relay | tinyint | NO | | 0 | |
| media-type | Type of media in a bit array (1 = image, 2 = video, 4 = audio | tinyint | NO | | 0 | |
| language | Language information about this post | varbinary(128) | YES | | NULL | |
| searchtext | Simplified text for the full text search | mediumtext | YES | | NULL | |
| created | | datetime | YES | | NULL | |
| restricted | If true, this post is either unlisted or not from a federated network | boolean | NO | | 0 | |
| comments | Number of comments | mediumint unsigned | YES | | NULL | |
@ -21,11 +22,12 @@ Fields
Indexes
------------
| Name | Fields |
| -------- | -------- |
| PRIMARY | uri-id |
| owner-id | owner-id |
| created | created |
| Name | Fields |
| ---------- | -------------------- |
| PRIMARY | uri-id |
| owner-id | owner-id |
| created | created |
| searchtext | FULLTEXT, searchtext |
Foreign Keys
------------

View file

@ -17,6 +17,7 @@ Friendica - Dokumentation und Ressourcen
* [Circles und Privatsphäre](help/Circles-and-Privacy)
* [Tags und Erwähnungen](help/Tags-and-Mentions)
* [Community-Gruppen](help/Groups)
* [Channels](help/Channels)
* [Chats](help/Chats)
* Weiterführende Informationen
* [Account umziehen](help/Move-Account)

View file

@ -0,0 +1,26 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Content\Conversation\Collection;
class UserDefinedChannels extends Timelines
{
}

View file

@ -0,0 +1,34 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Content\Conversation\Entity;
class Channel extends Timeline
{
const WHATSHOT = 'whatshot';
const FORYOU = 'foryou';
const FOLLOWERS = 'followers';
const SHARERSOFSHARERS = 'sharersofsharers';
const IMAGE = 'image';
const VIDEO = 'video';
const AUDIO = 'audio';
const LANGUAGE = 'language';
}

View file

@ -0,0 +1,28 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Content\Conversation\Entity;
final class Community extends Timeline
{
const LOCAL = 'local';
const GLOBAL = 'global';
}

View file

@ -0,0 +1,31 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Content\Conversation\Entity;
final class Network extends Timeline
{
const STAR = 'star';
const MENTION = 'mention';
const RECEIVED = 'received';
const COMMENTED = 'commented';
const CREATED = 'created';
}

View file

@ -22,30 +22,20 @@
namespace Friendica\Content\Conversation\Entity;
/**
* @property-read string $code Channel code
* @property-read string $label Channel label
* @property-read string $description Channel description
* @property-read string $accessKey Access key
* @property-read string $path Path
* @property-read string $code Channel code
* @property-read string $label Channel label
* @property-read string $description Channel description
* @property-read string $accessKey Access key
* @property-read string $path Path
* @property-read int $uid User of the channel
* @property-read string $includeTags The tags to include in the channel
* @property-read string $excludeTags The tags to exclude in the channel
* @property-read string $fullTextSearch full text search pattern
* @property-read int $mediaType Media types that are included in the channel
* @property-read int $circle Circle or timeline this channel is based on
*/
final class Timeline extends \Friendica\BaseEntity
class Timeline extends \Friendica\BaseEntity
{
const WHATSHOT = 'whatshot';
const FORYOU = 'foryou';
const FOLLOWERS = 'followers';
const SHARERSOFSHARERS = 'sharersofsharers';
const IMAGE = 'image';
const VIDEO = 'video';
const AUDIO = 'audio';
const LANGUAGE = 'language';
const LOCAL = 'local';
const GLOBAL = 'global';
const STAR = 'star';
const MENTION = 'mention';
const RECEIVED = 'received';
const COMMENTED = 'commented';
const CREATED = 'created';
/** @var string */
protected $code;
/** @var string */
@ -56,13 +46,31 @@ final class Timeline extends \Friendica\BaseEntity
protected $accessKey;
/** @var string */
protected $path;
/** @var int */
protected $uid;
/** @var int */
protected $circle;
/** @var string */
protected $includeTags;
/** @var string */
protected $excludeTags;
/** @var string */
protected $fullTextSearch;
/** @var int */
protected $mediaType;
public function __construct(string $code, string $label, string $description, string $accessKey, string $path = null)
public function __construct(string $code = null, string $label = null, string $description = null, string $accessKey = null, string $path = null, int $uid = null, string $includeTags = null, string $excludeTags = null, string $fullTextSearch = null, int $mediaType = null, int $circle = null)
{
$this->code = $code;
$this->label = $label;
$this->description = $description;
$this->accessKey = $accessKey;
$this->path = $path;
$this->code = $code;
$this->label = $label;
$this->description = $description;
$this->accessKey = $accessKey;
$this->path = $path;
$this->uid = $uid;
$this->includeTags = $includeTags;
$this->excludeTags = $excludeTags;
$this->fullTextSearch = $fullTextSearch;
$this->mediaType = $mediaType;
$this->circle = $circle;
}
}

View file

@ -0,0 +1,26 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Content\Conversation\Entity;
class UserDefinedChannel extends Channel
{
}

View file

@ -0,0 +1,59 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Content\Conversation\Factory;
use Friendica\Content\Conversation\Collection\Timelines;
use Friendica\Content\Conversation\Entity\Channel as ChannelEntity;
use Friendica\Model\User;
final class Channel extends Timeline
{
/**
* List of available channels
*
* @param integer $uid
* @return Timelines
*/
public function getTimelines(int $uid): Timelines
{
$language = User::getLanguageCode($uid);
$languages = $this->l10n->getAvailableLanguages(true);
$tabs = [
new ChannelEntity(ChannelEntity::FORYOU, $this->l10n->t('For you'), $this->l10n->t('Posts from contacts you interact with and who interact with you'), 'y'),
new ChannelEntity(ChannelEntity::WHATSHOT, $this->l10n->t('What\'s Hot'), $this->l10n->t('Posts with a lot of interactions'), 'h'),
new ChannelEntity(ChannelEntity::LANGUAGE, $languages[$language], $this->l10n->t('Posts in %s', $languages[$language]), 'g'),
new ChannelEntity(ChannelEntity::FOLLOWERS, $this->l10n->t('Followers'), $this->l10n->t('Posts from your followers that you don\'t follow'), 'f'),
new ChannelEntity(ChannelEntity::SHARERSOFSHARERS, $this->l10n->t('Sharers of sharers'), $this->l10n->t('Posts from accounts that are followed by accounts that you follow'), 'r'),
new ChannelEntity(ChannelEntity::IMAGE, $this->l10n->t('Images'), $this->l10n->t('Posts with images'), 'i'),
new ChannelEntity(ChannelEntity::AUDIO, $this->l10n->t('Audio'), $this->l10n->t('Posts with audio'), 'd'),
new ChannelEntity(ChannelEntity::VIDEO, $this->l10n->t('Videos'), $this->l10n->t('Posts with videos'), 'v'),
];
return new Timelines($tabs);
}
public function isTimeline(string $selectedTab): bool
{
return in_array($selectedTab, [ChannelEntity::WHATSHOT, ChannelEntity::FORYOU, ChannelEntity::FOLLOWERS, ChannelEntity::SHARERSOFSHARERS, ChannelEntity::IMAGE, ChannelEntity::VIDEO, ChannelEntity::AUDIO, ChannelEntity::LANGUAGE]);
}
}

View file

@ -0,0 +1,56 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Content\Conversation\Factory;
use Friendica\Content\Conversation\Collection\Timelines;
use Friendica\Content\Conversation\Entity\Community as CommunityEntity;
use Friendica\Module\Conversation\Community as CommunityModule;
final class Community extends Timeline
{
/**
* List of available communities
*
* @param boolean $authenticated
* @return Timelines
*/
public function getTimelines(bool $authenticated): Timelines
{
$page_style = $this->config->get('system', 'community_page_style');
$tabs = [];
if (($authenticated || in_array($page_style, [CommunityModule::LOCAL_AND_GLOBAL, CommunityModule::LOCAL])) && empty($this->config->get('system', 'singleuser'))) {
$tabs[] = new CommunityEntity(CommunityEntity::LOCAL, $this->l10n->t('Local Community'), $this->l10n->t('Posts from local users on this server'), 'l');
}
if ($authenticated || in_array($page_style, [CommunityModule::LOCAL_AND_GLOBAL, CommunityModule::GLOBAL])) {
$tabs[] = new CommunityEntity(CommunityEntity::GLOBAL, $this->l10n->t('Global Community'), $this->l10n->t('Posts from users of the whole federated network'), 'g');
}
return new Timelines($tabs);
}
public function isTimeline(string $selectedTab): bool
{
return in_array($selectedTab, [CommunityEntity::LOCAL, CommunityEntity::GLOBAL]);
}
}

View file

@ -0,0 +1,51 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Content\Conversation\Factory;
use Friendica\Content\Conversation\Collection\Timelines;
use Friendica\Content\Conversation\Entity\Network as NetworkEntity;
final class Network extends Timeline
{
/**
* List of available network timelines
*
* @param string $command
* @return Timelines
*/
public function getTimelines(string $command): Timelines
{
$tabs = [
new NetworkEntity(NetworkEntity::COMMENTED, $this->l10n->t('Latest Activity'), $this->l10n->t('Sort by latest activity'), 'e', $command . '?' . http_build_query(['order' => 'commented'])),
new NetworkEntity(NetworkEntity::RECEIVED, $this->l10n->t('Latest Posts'), $this->l10n->t('Sort by post received date'), 't', $command . '?' . http_build_query(['order' => 'received'])),
new NetworkEntity(NetworkEntity::CREATED, $this->l10n->t('Latest Creation'), $this->l10n->t('Sort by post creation date'), 'q', $command . '?' . http_build_query(['order' => 'created'])),
new NetworkEntity(NetworkEntity::MENTION, $this->l10n->t('Personal'), $this->l10n->t('Posts that mention or involve you'), 'r', $command . '?' . http_build_query(['mention' => true])),
new NetworkEntity(NetworkEntity::STAR, $this->l10n->t('Starred'), $this->l10n->t('Favourite Posts'), 'm', $command . '?' . http_build_query(['star' => true])),
];
return new Timelines($tabs);
}
public function isTimeline(string $selectedTab): bool
{
return in_array($selectedTab, [NetworkEntity::COMMENTED, NetworkEntity::RECEIVED, NetworkEntity::CREATED, NetworkEntity::MENTION, NetworkEntity::STAR]);
}
}

View file

@ -21,100 +21,28 @@
namespace Friendica\Content\Conversation\Factory;
use Friendica\Content\Conversation\Collection\Timelines;
use Friendica\Model\User;
use Friendica\Capabilities\ICanCreateFromTableRow;
use Friendica\Content\Conversation\Entity\Timeline as TimelineEntity;
use Friendica\Content\Conversation\Repository\UserDefinedChannel;
use Friendica\Core\Config\Capability\IManageConfigValues;
use Friendica\Core\L10n;
use Friendica\Module\Conversation\Community;
use Psr\Log\LoggerInterface;
final class Timeline extends \Friendica\BaseFactory
class Timeline extends \Friendica\BaseFactory
{
/** @var L10n */
protected $l10n;
/** @var IManageConfigValues The config */
protected $config;
/** @var UserDefinedChannel */
protected $channelRepository;
public function __construct(L10n $l10n, LoggerInterface $logger, IManageConfigValues $config)
public function __construct(UserDefinedChannel $channel, L10n $l10n, LoggerInterface $logger, IManageConfigValues $config)
{
parent::__construct($logger);
$this->l10n = $l10n;
$this->config = $config;
}
/**
* List of available channels
*
* @param integer $uid
* @return Timelines
*/
public function getChannelsForUser(int $uid): Timelines
{
$language = User::getLanguageCode($uid);
$languages = $this->l10n->getAvailableLanguages(true);
$tabs = [
new TimelineEntity(TimelineEntity::FORYOU, $this->l10n->t('For you'), $this->l10n->t('Posts from contacts you interact with and who interact with you'), 'y'),
new TimelineEntity(TimelineEntity::WHATSHOT, $this->l10n->t('What\'s Hot'), $this->l10n->t('Posts with a lot of interactions'), 'h'),
new TimelineEntity(TimelineEntity::LANGUAGE, $languages[$language], $this->l10n->t('Posts in %s', $languages[$language]), 'g'),
new TimelineEntity(TimelineEntity::FOLLOWERS, $this->l10n->t('Followers'), $this->l10n->t('Posts from your followers that you don\'t follow'), 'f'),
new TimelineEntity(TimelineEntity::SHARERSOFSHARERS, $this->l10n->t('Sharers of sharers'), $this->l10n->t('Posts from accounts that are followed by accounts that you follow'), 'r'),
new TimelineEntity(TimelineEntity::IMAGE, $this->l10n->t('Images'), $this->l10n->t('Posts with images'), 'i'),
new TimelineEntity(TimelineEntity::AUDIO, $this->l10n->t('Audio'), $this->l10n->t('Posts with audio'), 'd'),
new TimelineEntity(TimelineEntity::VIDEO, $this->l10n->t('Videos'), $this->l10n->t('Posts with videos'), 'v'),
];
return new Timelines($tabs);
}
/**
* List of available communities
*
* @param boolean $authenticated
* @return Timelines
*/
public function getCommunities(bool $authenticated): Timelines
{
$page_style = $this->config->get('system', 'community_page_style');
$tabs = [];
if (($authenticated || in_array($page_style, [Community::LOCAL_AND_GLOBAL, Community::LOCAL])) && empty($this->config->get('system', 'singleuser'))) {
$tabs[] = new TimelineEntity(TimelineEntity::LOCAL, $this->l10n->t('Local Community'), $this->l10n->t('Posts from local users on this server'), 'l');
}
if ($authenticated || in_array($page_style, [Community::LOCAL_AND_GLOBAL, Community::GLOBAL])) {
$tabs[] = new TimelineEntity(TimelineEntity::GLOBAL, $this->l10n->t('Global Community'), $this->l10n->t('Posts from users of the whole federated network'), 'g');
}
return new Timelines($tabs);
}
/**
* List of available network feeds
*
* @param string $command
* @return Timelines
*/
public function getNetworkFeeds(string $command): Timelines
{
$tabs = [
new TimelineEntity(TimelineEntity::COMMENTED, $this->l10n->t('Latest Activity'), $this->l10n->t('Sort by latest activity'), 'e', $command . '?' . http_build_query(['order' => 'commented'])),
new TimelineEntity(TimelineEntity::RECEIVED, $this->l10n->t('Latest Posts'), $this->l10n->t('Sort by post received date'), 't', $command . '?' . http_build_query(['order' => 'received'])),
new TimelineEntity(TimelineEntity::CREATED, $this->l10n->t('Latest Creation'), $this->l10n->t('Sort by post creation date'), 'q', $command . '?' . http_build_query(['order' => 'created'])),
new TimelineEntity(TimelineEntity::MENTION, $this->l10n->t('Personal'), $this->l10n->t('Posts that mention or involve you'), 'r', $command . '?' . http_build_query(['mention' => true])),
new TimelineEntity(TimelineEntity::STAR, $this->l10n->t('Starred'), $this->l10n->t('Favourite Posts'), 'm', $command . '?' . http_build_query(['star' => true])),
];
return new Timelines($tabs);
}
public function isCommunity(string $selectedTab): bool
{
return in_array($selectedTab, [TimelineEntity::LOCAL, TimelineEntity::GLOBAL]);
}
public function isChannel(string $selectedTab): bool
{
return in_array($selectedTab, [TimelineEntity::WHATSHOT, TimelineEntity::FORYOU, TimelineEntity::FOLLOWERS, TimelineEntity::SHARERSOFSHARERS, TimelineEntity::IMAGE, TimelineEntity::VIDEO, TimelineEntity::AUDIO, TimelineEntity::LANGUAGE]);
$this->channelRepository = $channel;
$this->l10n = $l10n;
$this->config = $config;
}
}

View file

@ -0,0 +1,51 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Content\Conversation\Factory;
use Friendica\Capabilities\ICanCreateFromTableRow;
use Friendica\Content\Conversation\Collection\Timelines;
use Friendica\Content\Conversation\Entity;
final class UserDefinedChannel extends Timeline implements ICanCreateFromTableRow
{
public function isTimeline(string $selectedTab, int $uid): bool
{
return is_numeric($selectedTab) && $uid && $this->channelRepository->existsById($selectedTab, $uid);
}
public function createFromTableRow(array $row): Entity\UserDefinedChannel
{
return new Entity\UserDefinedChannel(
$row['id'] ?? null,
$row['label'],
$row['description'] ?? null,
$row['access-key'] ?? null,
null,
$row['uid'],
$row['include-tags'] ?? null,
$row['exclude-tags'] ?? null,
$row['full-text-search'] ?? null,
$row['media-type'] ?? null,
$row['circle'] ?? null,
);
}
}

View file

@ -0,0 +1,133 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Content\Conversation\Repository;
use Friendica\BaseCollection;
use Friendica\Content\Conversation\Collection\UserDefinedChannels;
use Friendica\Content\Conversation\Entity;
use Friendica\Content\Conversation\Factory;
use Friendica\Database\Database;
use Psr\Log\LoggerInterface;
class UserDefinedChannel extends \Friendica\BaseRepository
{
protected static $table_name = 'channel';
public function __construct(Database $database, LoggerInterface $logger, Factory\UserDefinedChannel $factory)
{
parent::__construct($database, $logger, $factory);
}
/**
* @param array $condition
* @param array $params
* @return UserDefinedChannels
* @throws \Exception
*/
protected function _select(array $condition, array $params = []): BaseCollection
{
$rows = $this->db->selectToArray(static::$table_name, [], $condition, $params);
$Entities = new UserDefinedChannels();
foreach ($rows as $fields) {
$Entities[] = $this->factory->createFromTableRow($fields);
}
return $Entities;
}
/**
* Fetch a single user channel
*
* @param int $id The id of the user defined channel
* @param int $uid The user that this channel belongs to. (Not part of the primary key)
* @return Entity\UserDefinedChannel
* @throws \Friendica\Network\HTTPException\NotFoundException
*/
public function selectById(int $id, int $uid): Entity\UserDefinedChannel
{
return $this->_selectOne(['id' => $id, 'uid' => $uid]);
}
/**
* Checks if the provided channel id exists for this user
*
* @param integer $id
* @param integer $uid
* @return boolean
*/
public function existsById(int $id, int $uid): bool
{
return $this->exists(['id' => $id, 'uid' => $uid]);
}
/**
* Delete the given channel
*
* @param integer $id
* @param integer $uid
* @return boolean
*/
public function deleteById(int $id, int $uid): bool
{
return $this->db->delete('channel', ['id' => $id, 'uid' => $uid]);
}
/**
* Fetch all user channels
*
* @param integer $uid
* @return UserDefinedChannels
* @throws \Exception
*/
public function selectByUid(int $uid): UserDefinedChannels
{
return $this->_select(['uid' => $uid]);
}
public function save(Entity\UserDefinedChannel $Channel): Entity\UserDefinedChannel
{
$fields = [
'label' => $Channel->label,
'description' => $Channel->description,
'access-key' => $Channel->accessKey,
'uid' => $Channel->uid,
'circle' => $Channel->circle,
'include-tags' => $Channel->includeTags,
'exclude-tags' => $Channel->excludeTags,
'full-text-search' => $Channel->fullTextSearch,
'media-type' => $Channel->mediaType,
];
if ($Channel->code) {
$this->db->update(self::$table_name, $fields, ['uid' => $Channel->uid, 'id' => $Channel->code]);
} else {
$this->db->insert(self::$table_name, $fields, Database::INSERT_IGNORE);
$newChannelId = $this->db->lastInsertId();
$Channel = $this->selectById($newChannelId, $Channel->uid);
}
return $Channel;
}
}

View file

@ -560,12 +560,30 @@ class Widget
{
$channels = [];
foreach (DI::TimelineFactory()->getChannelsForUser($uid) as $channel) {
$channels[] = ['ref' => $channel->code, 'name' => $channel->label];
$enabled = DI::pConfig()->get($uid, 'system', 'enabled_timelines', []);
foreach (DI::NetworkFactory()->getTimelines('') as $channel) {
if (empty($enabled) || in_array($channel->code, $enabled)) {
$channels[] = ['ref' => $channel->code, 'name' => $channel->label];
}
}
foreach (DI::TimelineFactory()->getCommunities(true) as $community) {
$channels[] = ['ref' => $community->code, 'name' => $community->label];
foreach (DI::ChannelFactory()->getTimelines($uid) as $channel) {
if (empty($enabled) || in_array($channel->code, $enabled)) {
$channels[] = ['ref' => $channel->code, 'name' => $channel->label];
}
}
foreach (DI::userDefinedChannel()->selectByUid($uid) as $channel) {
if (empty($enabled) || in_array($channel->code, $enabled)) {
$channels[] = ['ref' => $channel->code, 'name' => $channel->label];
}
}
foreach (DI::CommunityFactory()->getTimelines(true) as $community) {
if (empty($enabled) || in_array($community->code, $enabled)) {
$channels[] = ['ref' => $community->code, 'name' => $community->label];
}
}
return self::filter(

View file

@ -102,8 +102,15 @@ class VCard
}
if ($contact['contact-type'] == Contact::TYPE_COMMUNITY) {
$showgroup_link = 'network/group/' . $id;
$mention_label = DI::l10n()->t('Post to group');
$mention_url = 'compose/0?body=!' . $contact['addr'];
$showgroup_label = DI::l10n()->t('View group');
$showgroup_url = 'network/group/' . $id;
} else {
$mention_label = DI::l10n()->t('Mention');
$mention_url = 'compose/0?body=@' . $contact['addr'];
}
}
return Renderer::replaceMacros(Renderer::getMarkupTemplate('widget/vcard.tpl'), [
@ -124,10 +131,8 @@ class VCard
'$unfollow_link' => $unfollow_link,
'$wallmessage' => DI::l10n()->t('Message'),
'$wallmessage_link' => $wallmessage_link,
'$mention' => DI::l10n()->t('Mention'),
'$posttogroup' => DI::l10n()->t('Post to group'),
'$showgroup' => DI::l10n()->t('View group'),
'$showgroup_link' => $showgroup_link,
'$mention' => [$mention_label, $mention_url],
'$showgroup' => [$showgroup_label, $showgroup_url]
]);
}
}

View file

@ -555,6 +555,35 @@ abstract class DI
return self::$dice->create(Content\Conversation\Factory\Timeline::class);
}
/**
* @return Content\Conversation\Factory\Community
*/
public static function CommunityFactory()
{
return self::$dice->create(Content\Conversation\Factory\Community::class);
}
/**
* @return Content\Conversation\Factory\Channel
*/
public static function ChannelFactory()
{
return self::$dice->create(Content\Conversation\Factory\Channel::class);
}
public static function userDefinedChannel(): Content\Conversation\Repository\UserDefinedChannel
{
return self::$dice->create(Content\Conversation\Repository\UserDefinedChannel::class);
}
/**
* @return Content\Conversation\Factory\Network
*/
public static function NetworkFactory()
{
return self::$dice->create(Content\Conversation\Factory\Network::class);
}
/**
* @return Contact\Introduction\Repository\Introduction
*/

View file

@ -1198,20 +1198,16 @@ class Contact
$pm_url = 'message/new/' . $contact['id'];
}
if ($contact['contact-type'] == Contact::TYPE_COMMUNITY) {
$mention_label = DI::l10n()->t('Post to group');
$mention_url = 'compose/0?body=!' . $contact['addr'];
} else {
$mention_label = DI::l10n()->t('Mention');
$mention_url = 'compose/0?body=@' . $contact['addr'];
}
$contact_url = 'contact/' . $contact['id'];
if ($contact['contact-type'] == Contact::TYPE_COMMUNITY) {
$mention_label = DI::l10n()->t('Post to group');
$mention_url = 'compose/0?body=!' . $contact['addr'];
$network_label = DI::l10n()->t('View group');
$network_url = 'network/group/' . $contact['id'];
} else {
$mention_label = DI::l10n()->t('Mention');
$mention_url = 'compose/0?body=@' . $contact['addr'];
$network_label = DI::l10n()->t('Network Posts');
$network_url = 'contact/' . $contact['id'] . '/conversations';
}

View file

@ -21,6 +21,7 @@
namespace Friendica\Model\Post;
use Friendica\Content\Text\BBCode;
use Friendica\Core\Logger;
use Friendica\Core\Protocol;
use Friendica\Database\Database;
@ -34,6 +35,7 @@ use Friendica\Model\Verb;
use Friendica\Protocol\Activity;
use Friendica\Protocol\Relay;
use Friendica\Util\DateTimeFormat;
use Friendica\Util\Strings;
// Channel
@ -52,9 +54,11 @@ class Engagement
return;
}
$parent = Post::selectFirst(['created', 'owner-id', 'uid', 'private', 'contact-contact-type', 'language'], ['uri-id' => $item['parent-uri-id']]);
$parent = Post::selectFirst(['uri-id', 'created', 'author-id', 'owner-id', 'uid', 'private', 'contact-contact-type', 'language', 'network',
'title', 'content-warning', 'body', 'author-contact-type', 'author-nick', 'author-addr', 'owner-contact-type', 'owner-nick', 'owner-addr'],
['uri-id' => $item['parent-uri-id']]);
if ($parent['created'] < DateTimeFormat::utc('now - ' . DI::config()->get('channel', 'engagement_hours') . ' hour')) {
if ($parent['created'] < self::getCreationDateLimit(false)) {
Logger::debug('Post is too old', ['uri-id' => $item['uri-id'], 'parent-uri-id' => $item['parent-uri-id'], 'created' => $parent['created']]);
return;
}
@ -87,6 +91,7 @@ class Engagement
'contact-type' => $parent['contact-contact-type'],
'media-type' => $mediatype,
'language' => $parent['language'],
'searchtext' => self::getSearchText($parent),
'created' => $parent['created'],
'restricted' => !in_array($item['network'], Protocol::FEDERATED) || ($parent['private'] != Item::PUBLIC),
'comments' => DBA::count('post', ['parent-uri-id' => $item['parent-uri-id'], 'gravity' => Item::GRAVITY_COMMENT]),
@ -104,6 +109,69 @@ class Engagement
Logger::debug('Engagement stored', ['fields' => $engagement, 'ret' => $ret]);
}
private static function getSearchText(array $item): string
{
$body = '[nosmile]network:' . $item['network'];
switch ($item['private']) {
case Item::PUBLIC:
$body .= ' visibility:public';
break;
case Item::UNLISTED:
$body .= ' visibility:unlisted';
break;
case Item::PRIVATE:
$body .= ' visibility:private';
break;
}
if ($item['author-contact-type'] == Contact::TYPE_COMMUNITY) {
$body .= ' group:' . $item['author-nick'] . ' group:' . $item['author-addr'];
} elseif (in_array($item['author-contact-type'], [Contact::TYPE_PERSON, Contact::TYPE_NEWS, Contact::TYPE_ORGANISATION])) {
$body .= ' from:' . $item['author-nick'] . ' from:' . $item['author-addr'];
}
if ($item['author-id'] != $item['owner-id']) {
if ($item['owner-contact-type'] == Contact::TYPE_COMMUNITY) {
$body .= ' group:' . $item['owner-nick'] . ' group:' . $item['owner-addr'];
} elseif (in_array($item['owner-contact-type'], [Contact::TYPE_PERSON, Contact::TYPE_NEWS, Contact::TYPE_ORGANISATION])) {
$body .= ' from:' . $item['owner-nick'] . ' from:' . $item['owner-addr'];
}
}
foreach (Tag::getByURIId($item['uri-id'], [Tag::MENTION, Tag::IMPLICIT_MENTION, Tag::EXCLUSIVE_MENTION, Tag::AUDIENCE]) as $tag) {
$contact = Contact::getByURL($tag['name'], false, ['nick', 'addr', 'contact-type']);
if (empty($contact)) {
continue;
}
if (($contact['contact-type'] == Contact::TYPE_COMMUNITY) && !strpos($body, 'group:' . $contact['addr'])) {
$body .= ' group:' . $contact['nick'] . ' group:' . $contact['addr'];
} elseif (in_array($contact['contact-type'], [Contact::TYPE_PERSON, Contact::TYPE_NEWS, Contact::TYPE_ORGANISATION])) {
$body .= ' to:' . $contact['nick'] . ' to:' . $contact['addr'];
}
}
foreach (Tag::getByURIId($item['uri-id'], [Tag::HASHTAG]) as $tag) {
$body .= ' tag:' . $tag['name'];
}
$body .= ' ' . $item['title'] . ' ' . $item['content-warning'] . ' ' . $item['body'];
$body = preg_replace("~\[url\=.*\]https?:.*\[\/url\]~", '', $body);
$body = Post\Media::addAttachmentsToBody($item['uri-id'], $body, [Post\Media::IMAGE]);
$text = BBCode::toPlaintext($body, false);
$text = preg_replace(Strings::autoLinkRegEx(), '', $text);
do {
$oldtext = $text;
$text = str_replace([' ', "\n", "\r"], ' ', $text);
} while ($oldtext != $text);
return $text;
}
private static function getMediaType(int $uri_id): int
{
$media = Post\Media::getByURIId($uri_id);
@ -127,7 +195,27 @@ class Engagement
*/
public static function expire()
{
DBA::delete('post-engagement', ["`created` < ?", DateTimeFormat::utc('now - ' . DI::config()->get('channel', 'engagement_hours') . ' hour')]);
Logger::notice('Cleared expired engagements', ['rows' => DBA::affectedRows()]);
$limit = self::getCreationDateLimit(true);
if (empty($limit)) {
Logger::notice('Expiration limit not reached');
return;
}
DBA::delete('post-engagement', ["`created` < ?", $limit]);
Logger::notice('Cleared expired engagements', ['limit' => $limit, 'rows' => DBA::affectedRows()]);
}
private static function getCreationDateLimit(bool $forDeletion): string
{
$posts = DI::config()->get('channel', 'engagement_post_limit');
if (!empty($posts)) {
$limit = DBA::selectToArray('post-engagement', ['created'], [], ['limit' => [$posts, 1], 'order' => ['created' => true]]);
if (!empty($limit)) {
return $limit[0]['created'];
} elseif ($forDeletion) {
return '';
}
}
return DateTimeFormat::utc('now - ' . DI::config()->get('channel', 'engagement_hours') . ' hour');
}
}

View file

@ -453,6 +453,18 @@ class Profile
Logger::warning('Missing hidewall key in profile array', ['profile' => $profile, 'callstack' => System::callstack(10)]);
}
if ($profile['account-type'] == Contact::TYPE_COMMUNITY) {
$mention_label = DI::l10n()->t('Post to group');
$mention_url = 'compose/0?body=!' . $profile['addr'];
$network_label = DI::l10n()->t('View group');
$network_url = 'network/group/' . $profile['id'];
} else {
$mention_label = DI::l10n()->t('Mention');
$mention_url = 'compose/0?body=@' . $profile['addr'];
$network_label = DI::l10n()->t('Network Posts');
$network_url = 'contact/' . $profile['id'] . '/conversations';
}
$tpl = Renderer::getMarkupTemplate('profile/vcard.tpl');
$o .= Renderer::replaceMacros($tpl, [
'$profile' => $p,
@ -476,6 +488,10 @@ class Profile
'$updated' => $updated,
'$diaspora' => $diaspora,
'$contact_block' => $contact_block,
'$mention_label' => $mention_label,
'$mention_url' => $mention_url,
'$network_label' => $network_label,
'$network_url' => $network_url,
]);
$arr = ['profile' => &$profile, 'entry' => &$o];

View file

@ -121,6 +121,13 @@ class BaseSettings extends BaseModule
'accesskey' => 'i',
];
$tabs[] = [
'label' => $this->t('Channels'),
'url' => 'settings/channels',
'selected' => static::class == Settings\Channels::class ? 'active' : '',
'accesskey' => '',
];
$tabs[] = [
'label' => $this->t('Social Networks'),
'url' => 'settings/connectors',

View file

@ -25,8 +25,13 @@ use Friendica\App;
use Friendica\App\Mode;
use Friendica\Content\BoundariesPager;
use Friendica\Content\Conversation;
use Friendica\Content\Conversation\Entity\Timeline as TimelineEntity;
use Friendica\Content\Conversation\Entity\Channel as ChannelEntity;
use Friendica\Content\Conversation\Factory\UserDefinedChannel as UserDefinedChannelFactory;
use Friendica\Content\Conversation\Factory\Timeline as TimelineFactory;
use Friendica\Content\Conversation\Repository\UserDefinedChannel as ChannelRepository;
use Friendica\Content\Conversation\Factory\Channel as ChannelFactory;
use Friendica\Content\Conversation\Factory\Community as CommunityFactory;
use Friendica\Content\Conversation\Factory\Network as NetworkFactory;
use Friendica\Content\Feature;
use Friendica\Content\Nav;
use Friendica\Content\Text\HTML;
@ -56,15 +61,27 @@ class Channel extends Timeline
protected $page;
/** @var SystemMessages */
protected $systemMessages;
/** @var ChannelFactory */
protected $channel;
/** @var UserDefinedChannelFactory */
protected $userDefinedChannel;
/** @var CommunityFactory */
protected $community;
/** @var NetworkFactory */
protected $networkFactory;
public function __construct(TimelineFactory $timeline, Conversation $conversation, App\Page $page, SystemMessages $systemMessages, Mode $mode, IHandleUserSessions $session, Database $database, IManagePersonalConfigValues $pConfig, IManageConfigValues $config, ICanCache $cache, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = [])
public function __construct(UserDefinedChannelFactory $userDefinedChannel, NetworkFactory $network, CommunityFactory $community, ChannelFactory $channelFactory, ChannelRepository $channel, TimelineFactory $timeline, Conversation $conversation, App\Page $page, SystemMessages $systemMessages, Mode $mode, IHandleUserSessions $session, Database $database, IManagePersonalConfigValues $pConfig, IManageConfigValues $config, ICanCache $cache, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = [])
{
parent::__construct($mode, $session, $database, $pConfig, $config, $cache, $l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
parent::__construct($channel, $mode, $session, $database, $pConfig, $config, $cache, $l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
$this->timeline = $timeline;
$this->conversation = $conversation;
$this->page = $page;
$this->systemMessages = $systemMessages;
$this->timeline = $timeline;
$this->conversation = $conversation;
$this->page = $page;
$this->systemMessages = $systemMessages;
$this->channel = $channelFactory;
$this->community = $community;
$this->networkFactory = $network;
$this->userDefinedChannel = $userDefinedChannel;
}
protected function content(array $request = []): string
@ -87,8 +104,9 @@ class Channel extends Timeline
}
if (empty($request['mode']) || ($request['mode'] != 'raw')) {
$tabs = $this->getTabArray($this->timeline->getChannelsForUser($this->session->getLocalUserId()), 'channel');
$tabs = array_merge($tabs, $this->getTabArray($this->timeline->getCommunities(true), 'channel'));
$tabs = $this->getTabArray($this->channel->getTimelines($this->session->getLocalUserId()), 'channel');
$tabs = array_merge($tabs, $this->getTabArray($this->channelRepository->selectByUid($this->session->getLocalUserId()), 'channel'));
$tabs = array_merge($tabs, $this->getTabArray($this->community->getTimelines(true), 'channel'));
$tab_tpl = Renderer::getMarkupTemplate('common_tabs.tpl');
$o .= Renderer::replaceMacros($tab_tpl, ['$tabs' => $tabs]);
@ -97,7 +115,7 @@ class Channel extends Timeline
$this->page['aside'] .= Widget::accountTypes('channel/' . $this->selectedTab, $this->accountTypeString);
if (!in_array($this->selectedTab, [TimelineEntity::FOLLOWERS, TimelineEntity::FORYOU]) && $this->config->get('system', 'community_no_sharer')) {
if (!in_array($this->selectedTab, [ChannelEntity::FOLLOWERS, ChannelEntity::FORYOU]) && $this->config->get('system', 'community_no_sharer')) {
$this->page['aside'] .= $this->getNoSharerWidget('channel');
}
@ -109,7 +127,7 @@ class Channel extends Timeline
$o .= $this->conversation->statusEditor([], 0, true);
}
if ($this->timeline->isChannel($this->selectedTab)) {
if ($this->channel->isTimeline($this->selectedTab) || $this->userDefinedChannel->isTimeline($this->selectedTab, $this->session->getLocalUserId())) {
$items = $this->getChannelItems();
$order = 'created';
} else {
@ -152,10 +170,10 @@ class Channel extends Timeline
parent::parseRequest($request);
if (!$this->selectedTab) {
$this->selectedTab = TimelineEntity::FORYOU;
$this->selectedTab = ChannelEntity::FORYOU;
}
if (!$this->timeline->isChannel($this->selectedTab) && !$this->timeline->isCommunity($this->selectedTab)) {
if (!$this->channel->isTimeline($this->selectedTab) && !$this->userDefinedChannel->isTimeline($this->selectedTab, $this->session->getLocalUserId()) && !$this->community->isTimeline($this->selectedTab)) {
throw new HTTPException\BadRequestException($this->l10n->t('Channel not available.'));
}

View file

@ -26,8 +26,9 @@ use Friendica\App;
use Friendica\App\Mode;
use Friendica\Content\BoundariesPager;
use Friendica\Content\Conversation;
use Friendica\Content\Conversation\Entity\Timeline as TimelineEntity;
use Friendica\Content\Conversation\Factory\Timeline as TimelineFactory;
use Friendica\Content\Conversation\Entity\Community as CommunityEntity;
use Friendica\Content\Conversation\Factory\Community as CommunityFactory;
use Friendica\Content\Conversation\Repository\UserDefinedChannel;
use Friendica\Content\Feature;
use Friendica\Content\Nav;
use Friendica\Content\Text\HTML;
@ -60,8 +61,8 @@ class Community extends Timeline
protected $pageStyle;
/** @var TimelineFactory */
protected $timeline;
/** @var CommunityFactory */
protected $community;
/** @var Conversation */
protected $conversation;
/** @var App\Page */
@ -69,11 +70,11 @@ class Community extends Timeline
/** @var SystemMessages */
protected $systemMessages;
public function __construct(TimelineFactory $timeline, Conversation $conversation, App\Page $page, SystemMessages $systemMessages, Mode $mode, IHandleUserSessions $session, Database $database, IManagePersonalConfigValues $pConfig, IManageConfigValues $config, ICanCache $cache, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = [])
public function __construct(UserDefinedChannel $channel, CommunityFactory $community, Conversation $conversation, App\Page $page, SystemMessages $systemMessages, Mode $mode, IHandleUserSessions $session, Database $database, IManagePersonalConfigValues $pConfig, IManageConfigValues $config, ICanCache $cache, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = [])
{
parent::__construct($mode, $session, $database, $pConfig, $config, $cache, $l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
parent::__construct($channel, $mode, $session, $database, $pConfig, $config, $cache, $l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
$this->timeline = $timeline;
$this->community = $community;
$this->conversation = $conversation;
$this->page = $page;
$this->systemMessages = $systemMessages;
@ -87,7 +88,7 @@ class Community extends Timeline
$o = Renderer::replaceMacros($t, [
'$content' => '',
'$header' => '',
'$show_global_community_hint' => ($this->selectedTab == TimelineEntity::GLOBAL) && $this->config->get('system', 'show_global_community_hint'),
'$show_global_community_hint' => ($this->selectedTab == CommunityEntity::GLOBAL) && $this->config->get('system', 'show_global_community_hint'),
'$global_community_hint' => $this->l10n->t("This community stream shows all public posts received by this node. They may not reflect the opinions of this nodes users.")
]);
@ -97,7 +98,7 @@ class Community extends Timeline
}
if (empty($request['mode']) || ($request['mode'] != 'raw')) {
$tabs = $this->getTabArray($this->timeline->getCommunities($this->session->isAuthenticated()), 'community');
$tabs = $this->getTabArray($this->community->getTimelines($this->session->isAuthenticated()), 'community');
$tab_tpl = Renderer::getMarkupTemplate('common_tabs.tpl');
$o .= Renderer::replaceMacros($tab_tpl, ['$tabs' => $tabs]);
@ -168,14 +169,14 @@ class Community extends Timeline
if (!$this->selectedTab) {
if (!empty($this->config->get('system', 'singleuser'))) {
// On single user systems only the global page does make sense
$this->selectedTab = TimelineEntity::GLOBAL;
$this->selectedTab = CommunityEntity::GLOBAL;
} else {
// When only the global community is allowed, we use this as default
$this->selectedTab = $this->pageStyle == self::GLOBAL ? TimelineEntity::GLOBAL : TimelineEntity::LOCAL;
$this->selectedTab = $this->pageStyle == self::GLOBAL ? CommunityEntity::GLOBAL : CommunityEntity::LOCAL;
}
}
if (!$this->timeline->isCommunity($this->selectedTab)) {
if (!$this->community->isTimeline($this->selectedTab)) {
throw new HTTPException\BadRequestException($this->l10n->t('Community option not available.'));
}
@ -184,11 +185,11 @@ class Community extends Timeline
$available = $this->pageStyle == self::LOCAL_AND_GLOBAL;
if (!$available) {
$available = ($this->pageStyle == self::LOCAL) && ($this->selectedTab == TimelineEntity::LOCAL);
$available = ($this->pageStyle == self::LOCAL) && ($this->selectedTab == CommunityEntity::LOCAL);
}
if (!$available) {
$available = ($this->pageStyle == self::GLOBAL) && ($this->selectedTab == TimelineEntity::GLOBAL);
$available = ($this->pageStyle == self::GLOBAL) && ($this->selectedTab == CommunityEntity::GLOBAL);
}
if (!$available) {

View file

@ -25,8 +25,13 @@ use Friendica\App;
use Friendica\App\Mode;
use Friendica\Content\BoundariesPager;
use Friendica\Content\Conversation;
use Friendica\Content\Conversation\Entity\Timeline as TimelineEntity;
use Friendica\Content\Conversation\Entity\Network as NetworkEntity;
use Friendica\Content\Conversation\Factory\Timeline as TimelineFactory;
use Friendica\Content\Conversation\Repository\UserDefinedChannel;
use Friendica\Content\Conversation\Factory\Channel as ChannelFactory;
use Friendica\Content\Conversation\Factory\UserDefinedChannel as UserDefinedChannelFactory;
use Friendica\Content\Conversation\Factory\Community as CommunityFactory;
use Friendica\Content\Conversation\Factory\Network as NetworkFactory;
use Friendica\Content\Feature;
use Friendica\Content\GroupManager;
use Friendica\Content\Nav;
@ -95,16 +100,28 @@ class Network extends Timeline
protected $database;
/** @var TimelineFactory */
protected $timeline;
/** @var ChannelFactory */
protected $channel;
/** @var UserDefinedChannelFactory */
protected $userDefinedChannel;
/** @var CommunityFactory */
protected $community;
/** @var NetworkFactory */
protected $networkFactory;
public function __construct(App $app, TimelineFactory $timeline, SystemMessages $systemMessages, Mode $mode, Conversation $conversation, App\Page $page, IHandleUserSessions $session, Database $database, IManagePersonalConfigValues $pConfig, IManageConfigValues $config, ICanCache $cache, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = [])
public function __construct(UserDefinedChannelFactory $userDefinedChannel, NetworkFactory $network, CommunityFactory $community, ChannelFactory $channelFactory, UserDefinedChannel $channel, App $app, TimelineFactory $timeline, SystemMessages $systemMessages, Mode $mode, Conversation $conversation, App\Page $page, IHandleUserSessions $session, Database $database, IManagePersonalConfigValues $pConfig, IManageConfigValues $config, ICanCache $cache, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = [])
{
parent::__construct($mode, $session, $database, $pConfig, $config, $cache, $l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
parent::__construct($channel, $mode, $session, $database, $pConfig, $config, $cache, $l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
$this->app = $app;
$this->timeline = $timeline;
$this->systemMessages = $systemMessages;
$this->conversation = $conversation;
$this->page = $page;
$this->app = $app;
$this->timeline = $timeline;
$this->systemMessages = $systemMessages;
$this->conversation = $conversation;
$this->page = $page;
$this->channel = $channelFactory;
$this->community = $community;
$this->networkFactory = $network;
$this->userDefinedChannel = $userDefinedChannel;
}
protected function content(array $request = []): string
@ -117,25 +134,14 @@ class Network extends Timeline
$module = 'network';
$this->page['aside'] .= Widget::channels($module, $this->selectedTab, $this->session->getLocalUserId());
$this->page['aside'] .= Widget::accountTypes($module, $this->accountTypeString);
$arr = ['query' => $this->args->getQueryString()];
Hook::callAll('network_content_init', $arr);
$o = '';
if ($this->timeline->isChannel($this->selectedTab)) {
if (!in_array($this->selectedTab, [TimelineEntity::FOLLOWERS, TimelineEntity::FORYOU]) && $this->config->get('system', 'community_no_sharer')) {
$this->page['aside'] .= $this->getNoSharerWidget($module);
}
if ($this->channel->isTimeline($this->selectedTab) || $this->userDefinedChannel->isTimeline($this->selectedTab, $this->session->getLocalUserId())) {
$items = $this->getChannelItems();
} elseif ($this->timeline->isCommunity($this->selectedTab)) {
if ($this->session->getLocalUserId() && $this->config->get('system', 'community_no_sharer')) {
$this->page['aside'] .= $this->getNoSharerWidget($module);
}
} elseif ($this->community->isTimeline($this->selectedTab)) {
$items = $this->getCommunityItems();
} else {
$items = $this->getItems();
@ -145,6 +151,8 @@ class Network extends Timeline
$this->page['aside'] .= GroupManager::widget($module . '/group', $this->session->getLocalUserId(), $this->groupContactId);
$this->page['aside'] .= Widget::postedByYear($module . '/archive', $this->session->getLocalUserId(), false);
$this->page['aside'] .= Widget::networks($module, !$this->groupContactId ? $this->network : '');
$this->page['aside'] .= Widget::accountTypes($module, $this->accountTypeString);
$this->page['aside'] .= Widget::channels($module, $this->selectedTab, $this->session->getLocalUserId());
$this->page['aside'] .= Widget\SavedSearches::getHTML($this->args->getQueryString());
$this->page['aside'] .= Widget::fileAs('filed', '');
@ -274,13 +282,13 @@ class Network extends Timeline
*/
private function getTabsHTML()
{
// @todo user confgurable selection of tabs
$tabs = $this->getTabArray($this->timeline->getNetworkFeeds($this->args->getCommand()), 'network');
$tabs = $this->getTabArray($this->networkFactory->getTimelines($this->args->getCommand()), 'network');
$network_timelines = $this->pConfig->get($this->session->getLocalUserId(), 'system', 'network_timelines', []);
if (!empty($network_timelines)) {
$tabs = array_merge($tabs, $this->getTabArray($this->timeline->getChannelsForUser($this->session->getLocalUserId()), 'network', 'channel'));
$tabs = array_merge($tabs, $this->getTabArray($this->timeline->getCommunities(true), 'network', 'channel'));
$tabs = array_merge($tabs, $this->getTabArray($this->channel->getTimelines($this->session->getLocalUserId()), 'network', 'channel'));
$tabs = array_merge($tabs, $this->getTabArray($this->channelRepository->selectByUid($this->session->getLocalUserId()), 'network', 'channel'));
$tabs = array_merge($tabs, $this->getTabArray($this->community->getTimelines(true), 'network', 'channel'));
}
$arr = ['tabs' => $tabs];
@ -289,9 +297,9 @@ class Network extends Timeline
if (!empty($network_timelines)) {
$tabs = [];
foreach (array_keys($arr['tabs']) as $tab) {
if (in_array($tab, $network_timelines)) {
$tabs[] = $arr['tabs'][$tab];
foreach ($arr['tabs'] as $tab) {
if (in_array($tab['code'], $network_timelines)) {
$tabs[] = $tab;
}
}
} else {
@ -313,26 +321,26 @@ class Network extends Timeline
if (!$this->selectedTab) {
$this->selectedTab = self::getTimelineOrderBySession($this->session, $this->pConfig);
} elseif (!$this->timeline->isChannel($this->selectedTab) && !$this->timeline->isCommunity($this->selectedTab)) {
} elseif (!$this->networkFactory->isTimeline($this->selectedTab) && !$this->channel->isTimeline($this->selectedTab) && !$this->userDefinedChannel->isTimeline($this->selectedTab, $this->session->getLocalUserId()) && !$this->community->isTimeline($this->selectedTab)) {
throw new HTTPException\BadRequestException($this->l10n->t('Network feed not available.'));
}
if (($this->network || $this->circleId || $this->groupContactId) && ($this->timeline->isChannel($this->selectedTab) || $this->timeline->isCommunity($this->selectedTab))) {
$this->selectedTab = TimelineEntity::RECEIVED;
if (($this->network || $this->circleId || $this->groupContactId) && ($this->channel->isTimeline($this->selectedTab) || $this->userDefinedChannel->isTimeline($this->selectedTab, $this->session->getLocalUserId()) || $this->community->isTimeline($this->selectedTab))) {
$this->selectedTab = NetworkEntity::RECEIVED;
}
if (!empty($request['star'])) {
$this->selectedTab = TimelineEntity::STAR;
$this->selectedTab = NetworkEntity::STAR;
$this->star = true;
} else {
$this->star = $this->selectedTab == TimelineEntity::STAR;
$this->star = $this->selectedTab == NetworkEntity::STAR;
}
if (!empty($request['mention'])) {
$this->selectedTab = TimelineEntity::MENTION;
$this->selectedTab = NetworkEntity::MENTION;
$this->mention = true;
} else {
$this->mention = $this->selectedTab == TimelineEntity::MENTION;
$this->mention = $this->selectedTab == NetworkEntity::MENTION;
}
if (!empty($request['order'])) {
@ -340,9 +348,9 @@ class Network extends Timeline
$this->order = $request['order'];
$this->star = false;
$this->mention = false;
} elseif (in_array($this->selectedTab, [TimelineEntity::RECEIVED, TimelineEntity::STAR]) || $this->timeline->isCommunity($this->selectedTab)) {
} elseif (in_array($this->selectedTab, [NetworkEntity::RECEIVED, NetworkEntity::STAR]) || $this->community->isTimeline($this->selectedTab)) {
$this->order = 'received';
} elseif (($this->selectedTab == TimelineEntity::CREATED) || $this->timeline->isChannel($this->selectedTab)) {
} elseif (($this->selectedTab == NetworkEntity::CREATED) || $this->channel->isTimeline($this->selectedTab) || $this->userDefinedChannel->isTimeline($this->selectedTab, $this->session->getLocalUserId())) {
$this->order = 'created';
} else {
$this->order = 'commented';
@ -352,16 +360,16 @@ class Network extends Timeline
// Upon updates in the background and order by last comment we order by received date,
// since otherwise the feed will optically jump, when some already visible thread has been updated.
if ($this->update && ($this->selectedTab == TimelineEntity::COMMENTED)) {
if ($this->update && ($this->selectedTab == NetworkEntity::COMMENTED)) {
$this->order = 'received';
$request['last_received'] = $request['last_commented'] ?? null;
$request['first_received'] = $request['first_commented'] ?? null;
}
// Prohibit combined usage of "star" and "mention"
if ($this->selectedTab == TimelineEntity::STAR) {
if ($this->selectedTab == NetworkEntity::STAR) {
$this->mention = false;
} elseif ($this->selectedTab == TimelineEntity::MENTION) {
} elseif ($this->selectedTab == NetworkEntity::MENTION) {
$this->star = false;
}

View file

@ -25,7 +25,8 @@ use Friendica\App;
use Friendica\App\Mode;
use Friendica\BaseModule;
use Friendica\Content\Conversation\Collection\Timelines;
use Friendica\Content\Conversation\Entity\Timeline as TimelineEntity;
use Friendica\Content\Conversation\Entity\Channel as ChannelEntity;
use Friendica\Content\Conversation\Repository\UserDefinedChannel;
use Friendica\Core\Cache\Capability\ICanCache;
use Friendica\Core\Cache\Enum\Duration;
use Friendica\Core\Config\Capability\IManageConfigValues;
@ -79,17 +80,20 @@ class Timeline extends BaseModule
protected $config;
/** @var ICanCache */
protected $cache;
/** @var UserDefinedChannel */
protected $channelRepository;
public function __construct(Mode $mode, IHandleUserSessions $session, Database $database, IManagePersonalConfigValues $pConfig, IManageConfigValues $config, ICanCache $cache, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = [])
public function __construct(UserDefinedChannel $channel, Mode $mode, IHandleUserSessions $session, Database $database, IManagePersonalConfigValues $pConfig, IManageConfigValues $config, ICanCache $cache, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = [])
{
parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
$this->mode = $mode;
$this->session = $session;
$this->database = $database;
$this->pConfig = $pConfig;
$this->config = $config;
$this->cache = $cache;
$this->channelRepository = $channel;
$this->mode = $mode;
$this->session = $session;
$this->database = $database;
$this->pConfig = $pConfig;
$this->config = $config;
$this->cache = $cache;
}
/**
@ -176,6 +180,7 @@ class Timeline extends BaseModule
$path = $tab->path ?? $prefix . '/' . $tab->code;
}
$tabs[$tab->code] = [
'code' => $tab->code,
'label' => $tab->label,
'url' => $path,
'sel' => $this->selectedTab == $tab->code ? 'active' : '',
@ -264,13 +269,13 @@ class Timeline extends BaseModule
{
$uid = $this->session->getLocalUserId();
if ($this->selectedTab == TimelineEntity::WHATSHOT) {
if ($this->selectedTab == ChannelEntity::WHATSHOT) {
if (!is_null($this->accountType)) {
$condition = ["(`comments` > ? OR `activities` > ?) AND `contact-type` = ?", $this->getMedianComments($uid, 4), $this->getMedianActivities($uid, 4), $this->accountType];
} else {
$condition = ["(`comments` > ? OR `activities` > ?) AND `contact-type` != ?", $this->getMedianComments($uid, 4), $this->getMedianActivities($uid, 4), Contact::TYPE_COMMUNITY];
}
} elseif ($this->selectedTab == TimelineEntity::FORYOU) {
} elseif ($this->selectedTab == ChannelEntity::FORYOU) {
$cid = Contact::getPublicIdByUserId($uid);
$condition = [
@ -280,9 +285,9 @@ class Timeline extends BaseModule
$cid, $this->getMedianRelationThreadScore($cid, 4), $this->getMedianComments($uid, 4), $this->getMedianActivities($uid, 4), $cid,
$uid, Contact\User::FREQUENCY_ALWAYS
];
} elseif ($this->selectedTab == TimelineEntity::FOLLOWERS) {
} elseif ($this->selectedTab == ChannelEntity::FOLLOWERS) {
$condition = ["`owner-id` IN (SELECT `pid` FROM `account-user-view` WHERE `uid` = ? AND `rel` = ?)", $uid, Contact::FOLLOWER];
} elseif ($this->selectedTab == TimelineEntity::SHARERSOFSHARERS) {
} elseif ($this->selectedTab == ChannelEntity::SHARERSOFSHARERS) {
$cid = Contact::getPublicIdByUserId($uid);
// @todo Suggest posts from contacts that are followed most by our followers
@ -292,17 +297,19 @@ class Timeline extends BaseModule
AND NOT `cid` IN (SELECT `cid` FROM `contact-relation` WHERE `follows` AND `relation-cid` = ?))",
DateTimeFormat::utc('now - ' . $this->config->get('channel', 'sharer_interaction_days') . ' day'), $cid, $this->getMedianRelationThreadScore($cid, 4), $cid
];
} elseif ($this->selectedTab == TimelineEntity::IMAGE) {
} elseif ($this->selectedTab == ChannelEntity::IMAGE) {
$condition = ["`media-type` & ?", 1];
} elseif ($this->selectedTab == TimelineEntity::VIDEO) {
} elseif ($this->selectedTab == ChannelEntity::VIDEO) {
$condition = ["`media-type` & ?", 2];
} elseif ($this->selectedTab == TimelineEntity::AUDIO) {
} elseif ($this->selectedTab == ChannelEntity::AUDIO) {
$condition = ["`media-type` & ?", 4];
} elseif ($this->selectedTab == TimelineEntity::LANGUAGE) {
} elseif ($this->selectedTab == ChannelEntity::LANGUAGE) {
$condition = ["JSON_EXTRACT(JSON_KEYS(language), '$[0]') = ?", $this->l10n->convertCodeForLanguageDetection(User::getLanguageCode($uid))];
} elseif (is_numeric($this->selectedTab)) {
$condition = $this->getUserChannelConditions($this->selectedTab, $this->session->getLocalUserId());
}
if ($this->selectedTab != TimelineEntity::LANGUAGE) {
if ($this->selectedTab != ChannelEntity::LANGUAGE) {
$condition = $this->addLanguageCondition($uid, $condition);
}
@ -310,7 +317,7 @@ class Timeline extends BaseModule
$condition = DBA::mergeConditions($condition, ["NOT EXISTS(SELECT `cid` FROM `user-contact` WHERE `uid` = ? AND `cid` = `post-engagement`.`owner-id` AND (`ignored` OR `blocked` OR `collapsed` OR `is-blocked` OR `channel-frequency` = ?))", $uid, Contact\User::FREQUENCY_NEVER]);
if (($this->selectedTab != TimelineEntity::WHATSHOT) && !is_null($this->accountType)) {
if (($this->selectedTab != ChannelEntity::WHATSHOT) && !is_null($this->accountType)) {
$condition = DBA::mergeConditions($condition, ['contact-type' => $this->accountType]);
}
@ -359,6 +366,53 @@ class Timeline extends BaseModule
return $items;
}
private function getUserChannelConditions(int $id, int $uid): array
{
$channel = $this->channelRepository->selectById($id, $uid);
if (empty($channel)) {
return [];
}
$condition = [];
if (!empty($channel->circle)) {
if ($channel->circle == -1) {
$condition = ["`owner-id` IN (SELECT `pid` FROM `account-user-view` WHERE `uid` = ? AND `rel` IN (?, ?))", $uid, Contact::SHARING, Contact::FRIEND];
} elseif ($channel->circle == -2) {
$condition = ["`owner-id` IN (SELECT `pid` FROM `account-user-view` WHERE `uid` = ? AND `rel` = ?)", $uid, Contact::FOLLOWER];
} elseif ($channel->circle > 0) {
$condition = DBA::mergeConditions($condition, ["`owner-id` IN (SELECT `pid` FROM `group_member` INNER JOIN `account-user-view` ON `group_member`.`contact-id` = `account-user-view`.`id` WHERE `gid` = ? AND `account-user-view`.`uid` = ?)", $channel->circle, $uid]);
}
}
if (!empty($channel->fullTextSearch)) {
$search = $channel->fullTextSearch;
foreach (['from', 'to', 'group', 'tag', 'network', 'visibility'] as $keyword) {
$search = preg_replace('~(' . $keyword . ':.[\w@\.-]+)~', '"$1"', $search);
}
$condition = DBA::mergeConditions($condition, ["MATCH (`searchtext`) AGAINST (? IN BOOLEAN MODE)", $search]);
}
if (!empty($channel->includeTags)) {
$search = explode(',', mb_strtolower($channel->includeTags));
$placeholders = substr(str_repeat("?, ", count($search)), 0, -2);
$condition = DBA::mergeConditions($condition, array_merge(["`uri-id` IN (SELECT `uri-id` FROM `post-tag` INNER JOIN `tag` ON `tag`.`id` = `post-tag`.`tid` WHERE `post-tag`.`type` = 1 AND `name` IN (" . $placeholders . "))"], $search));
}
if (!empty($channel->excludeTags)) {
$search = explode(',', mb_strtolower($channel->excludeTags));
$placeholders = substr(str_repeat("?, ", count($search)), 0, -2);
$condition = DBA::mergeConditions($condition, array_merge(["NOT `uri-id` IN (SELECT `uri-id` FROM `post-tag` INNER JOIN `tag` ON `tag`.`id` = `post-tag`.`tid` WHERE `post-tag`.`type` = 1 AND `name` IN (" . $placeholders . "))"], $search));
}
if (!empty($channel->mediaType)) {
$condition = DBA::mergeConditions($condition, ["`media-type` & ?", $channel->mediaType]);
}
// For "addLanguageCondition" to work, the condition must not be empty
return $condition ?: ["true"];
}
private function addLanguageCondition(int $uid, array $condition): array
{
$conditions = [];

View file

@ -0,0 +1,188 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Module\Settings;
use Friendica\App;
use Friendica\Content\Conversation\Factory;
use Friendica\Content\Conversation\Repository\UserDefinedChannel;
use Friendica\Core\L10n;
use Friendica\Core\Renderer;
use Friendica\Core\Session\Capability\IHandleUserSessions;
use Friendica\Model\Circle;
use Friendica\Module\BaseSettings;
use Friendica\Module\Response;
use Friendica\Network\HTTPException;
use Friendica\Util\Profiler;
use Psr\Log\LoggerInterface;
class Channels extends BaseSettings
{
/** @var UserDefinedChannel */
private $channel;
/** @var Factory\UserDefinedChannel */
private $userDefinedChannel;
public function __construct(Factory\UserDefinedChannel $userDefinedChannel, UserDefinedChannel $channel, App\Page $page, IHandleUserSessions $session, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = [])
{
parent::__construct($session, $page, $l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
$this->userDefinedChannel = $userDefinedChannel;
$this->channel = $channel;
}
protected function post(array $request = [])
{
$uid = $this->session->getLocalUserId();
if (!$uid) {
throw new HTTPException\ForbiddenException($this->t('Permission denied.'));
}
if (empty($request['edit_channel']) && empty($request['add_channel'])) {
return;
}
self::checkFormSecurityTokenRedirectOnError('/settings/channels', 'settings_channels');
if (!empty($request['add_channel'])) {
$channel = $this->userDefinedChannel->createFromTableRow([
'label' => $request['new_label'],
'description' => $request['new_description'],
'access-key' => substr(mb_strtolower($request['new_access_key']), 0, 1),
'uid' => $uid,
'circle' => (int)$request['new_circle'],
'include-tags' => $this->cleanTags($request['new_include_tags']),
'exclude-tags' => $this->cleanTags($request['new_exclude_tags']),
'full-text-search' => $this->cleanTags($request['new_text_search']),
'media-type' => ($request['new_image'] ? 1 : 0) | ($request['new_video'] ? 2 : 0) | ($request['new_audio'] ? 4 : 0),
]);
$saved = $this->channel->save($channel);
$this->logger->debug('New channel added', ['saved' => $saved]);
return;
}
foreach (array_keys($request['label']) as $id) {
if ($request['delete'][$id]) {
$success = $this->channel->deleteById($id, $uid);
$this->logger->debug('Channel deleted', ['id' => $id, 'success' => $success]);
continue;
}
$channel = $this->userDefinedChannel->createFromTableRow([
'id' => $id,
'label' => $request['label'][$id],
'description' => $request['description'][$id],
'access-key' => substr(mb_strtolower($request['access_key'][$id]), 0, 1),
'uid' => $uid,
'circle' => (int)$request['circle'][$id],
'include-tags' => $this->cleanTags($request['include_tags'][$id]),
'exclude-tags' => $this->cleanTags($request['exclude_tags'][$id]),
'full-text-search' => $this->cleanTags($request['text_search'][$id]),
'media-type' => ($request['image'][$id] ? 1 : 0) | ($request['video'][$id] ? 2 : 0) | ($request['audio'][$id] ? 4 : 0),
]);
$saved = $this->channel->save($channel);
$this->logger->debug('Save channel', ['id' => $id, 'saved' => $saved]);
}
$this->baseUrl->redirect('/settings/channels');
}
protected function content(array $request = []): string
{
parent::content();
$uid = $this->session->getLocalUserId();
if (!$uid) {
throw new HTTPException\ForbiddenException($this->t('Permission denied.'));
}
$circles = [
0 => $this->l10n->t('Global Community'),
-1 => $this->l10n->t('Following'),
-2 => $this->l10n->t('Followers'),
];
foreach (Circle::getByUserId($uid) as $circle) {
$circles[$circle['id']] = $circle['name'];
}
$blocklistform = [];
foreach ($this->channel->selectByUid($uid) as $channel) {
$blocklistform[] = [
'label' => ["label[$channel->code]", $this->t('Label'), $channel->label, '', $this->t('Required')],
'description' => ["description[$channel->code]", $this->t("Description"), $channel->description],
'access_key' => ["access_key[$channel->code]", $this->t("Access Key"), $channel->accessKey],
'circle' => ["circle[$channel->code]", $this->t('Circle/Channel'), $channel->circle, '', $circles],
'include_tags' => ["include_tags[$channel->code]", $this->t("Include Tags"), $channel->includeTags],
'exclude_tags' => ["exclude_tags[$channel->code]", $this->t("Exclude Tags"), $channel->excludeTags],
'text_search' => ["text_search[$channel->code]", $this->t("Full Text Search"), $channel->fullTextSearch],
'image' => ["image[$channel->code]", $this->t("Images"), $channel->mediaType & 1],
'video' => ["video[$channel->code]", $this->t("Videos"), $channel->mediaType & 2],
'audio' => ["audio[$channel->code]", $this->t("Audio"), $channel->mediaType & 4],
'delete' => ["delete[$channel->code]", $this->t("Delete channel") . ' (' . $channel->label . ')', false, $this->t("Check to delete this entry from the channel list")]
];
}
$t = Renderer::getMarkupTemplate('settings/channels.tpl');
return Renderer::replaceMacros($t, [
'label' => ["new_label", $this->t('Label'), '', $this->t('Short name for the channel. It is displayed on the channels widget.'), $this->t('Required')],
'description' => ["new_description", $this->t("Description"), '', $this->t('This should describe the content of the channel in a few word.')],
'access_key' => ["new_access_key", $this->t("Access Key"), '', $this->t('When you want to access this channel via an access key, you can define it here. Pay attention to not use an already used one.')],
'circle' => ['new_circle', $this->t('Circle/Channel'), 0, $this->t('Select a circle or channel, that your channel should be based on.'), $circles],
'include_tags' => ["new_include_tags", $this->t("Include Tags"), '', $this->t('Comma separated list of tags. A post will be used when it contains any of the listed tags.')],
'exclude_tags' => ["new_exclude_tags", $this->t("Exclude Tags"), '', $this->t('Comma separated list of tags. If a post contain any of these tags, then it will not be part of nthis channel.')],
'text_search' => ["new_text_search", $this->t("Full Text Search"), '', $this->t('Search terms for the body, supports the "boolean mode" operators from MariaDB. See the help for a complete list of operators and additional keywords: %s', '<a href="help/Channels">help/Channels</a>')],
'image' => ['new_image', $this->t("Images"), false, $this->t("Check to display images in the channel.")],
'video' => ["new_video", $this->t("Videos"), false, $this->t("Check to display videos in the channel.")],
'audio' => ["new_audio", $this->t("Audio"), false, $this->t("Check to display audio in the channel.")],
'$l10n' => [
'title' => $this->t('Channels'),
'intro' => $this->t('This page can be used to define your own channels.'),
'addtitle' => $this->t('Add new entry to the channel list'),
'addsubmit' => $this->t('Add'),
'savechanges' => $this->t('Save'),
'currenttitle' => $this->t('Current Entries in the channel list'),
'thurl' => $this->t('Blocked server domain pattern'),
'threason' => $this->t('Reason for the block'),
'delentry' => $this->t('Delete entry from the channel list'),
'confirm_delete' => $this->t('Delete entry from the channel list?'),
],
'$entries' => $blocklistform,
'$baseurl' => $this->baseUrl,
'$form_security_token' => self::getFormSecurityToken('settings_channels'),
]);
}
private function cleanTags(string $tag_list): string
{
$tags = [];
$tagitems = explode(',', mb_strtolower($tag_list));
foreach ($tagitems as $tag) {
$tag = trim($tag, '# ');
if (!empty($tag)) {
$tags[] = $tag;
}
}
return implode(',', $tags);
}
}

View file

@ -22,8 +22,13 @@
namespace Friendica\Module\Settings;
use Friendica\App;
use Friendica\Content\Conversation\Collection\Timelines;
use Friendica\Content\Text\BBCode;
use Friendica\Content\Conversation\Factory\Channel as ChannelFactory;
use Friendica\Content\Conversation\Factory\Community as CommunityFactory;
use Friendica\Content\Conversation\Factory\Network as NetworkFactory;
use Friendica\Content\Conversation\Factory\Timeline as TimelineFactory;
use Friendica\Content\Conversation\Repository;
use Friendica\Core\Config\Capability\IManageConfigValues;
use Friendica\Core\Hook;
use Friendica\Core\L10n;
@ -52,18 +57,30 @@ class Display extends BaseSettings
private $app;
/** @var SystemMessages */
private $systemMessages;
/** @var ChannelFactory */
protected $channel;
/** @var Repository\UserDefinedChannel */
protected $userDefinedChannel;
/** @var CommunityFactory */
protected $community;
/** @var NetworkFactory */
protected $network;
/** @var TimelineFactory */
protected $timeline;
public function __construct(TimelineFactory $timeline, SystemMessages $systemMessages, App $app, IManagePersonalConfigValues $pConfig, IManageConfigValues $config, IHandleUserSessions $session, App\Page $page, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = [])
public function __construct(Repository\UserDefinedChannel $userDefinedChannel, NetworkFactory $network, CommunityFactory $community, ChannelFactory $channel, TimelineFactory $timeline, SystemMessages $systemMessages, App $app, IManagePersonalConfigValues $pConfig, IManageConfigValues $config, IHandleUserSessions $session, App\Page $page, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = [])
{
parent::__construct($session, $page, $l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
$this->config = $config;
$this->pConfig = $pConfig;
$this->app = $app;
$this->systemMessages = $systemMessages;
$this->timeline = $timeline;
$this->config = $config;
$this->pConfig = $pConfig;
$this->app = $app;
$this->systemMessages = $systemMessages;
$this->timeline = $timeline;
$this->channel = $channel;
$this->community = $community;
$this->network = $network;
$this->userDefinedChannel = $userDefinedChannel;
}
protected function post(array $request = [])
@ -80,7 +97,8 @@ class Display extends BaseSettings
$theme = !empty($request['theme']) ? trim($request['theme']) : $user['theme'];
$mobile_theme = !empty($request['mobile_theme']) ? trim($request['mobile_theme']) : '';
$enable_smile = !empty($request['enable_smile']) ? intval($request['enable_smile']) : 0;
$network_timelines = !empty($request['network_timelines']) ? $request['network_timelines'] : [];
$enable = !empty($request['enable']) ? $request['enable'] : [];
$bookmark = !empty($request['bookmark']) ? $request['bookmark'] : [];
$channel_languages = !empty($request['channel_languages']) ? $request['channel_languages'] : [];
$first_day_of_week = !empty($request['first_day_of_week']) ? intval($request['first_day_of_week']) : 0;
$calendar_default_view = !empty($request['calendar_default_view']) ? trim($request['calendar_default_view']) : 'month';
@ -98,6 +116,20 @@ class Display extends BaseSettings
}
}
$enabled_timelines = [];
foreach ($enable as $code => $enabled) {
if ($enabled) {
$enabled_timelines[] = $code;
}
}
$network_timelines = [];
foreach ($bookmark as $code => $bookmarked) {
if ($bookmarked) {
$network_timelines[] = $code;
}
}
$itemspage_network = !empty($request['itemspage_network']) ?
intval($request['itemspage_network']) :
$this->config->get('system', 'itemspage_network');
@ -127,6 +159,7 @@ class Display extends BaseSettings
$this->pConfig->set($uid, 'system', 'preview_mode' , $preview_mode);
$this->pConfig->set($uid, 'system', 'network_timelines' , $network_timelines);
$this->pConfig->set($uid, 'system', 'enabled_timelines' , $enabled_timelines);
$this->pConfig->set($uid, 'channel', 'languages' , $channel_languages);
$this->pConfig->set($uid, 'calendar', 'first_day_of_week' , $first_day_of_week);
@ -224,10 +257,20 @@ class Display extends BaseSettings
BBCode::PREVIEW_LARGE => $this->t('Large Image'),
];
$network_timelines = $this->pConfig->get($uid, 'system', 'network_timelines', array_keys($this->getAvailableTimelines($uid, true)));
$bookmarked_timelines = $this->pConfig->get($uid, 'system', 'network_timelines', $this->getAvailableTimelines($uid, true)->column('code'));
$enabled_timelines = $this->pConfig->get($uid, 'system', 'enabled_timelines', $this->getAvailableTimelines($uid, false)->column('code'));
$channel_languages = $this->pConfig->get($uid, 'channel', 'languages', [User::getLanguageCode($uid)]);
$languages = $this->l10n->getAvailableLanguages(true);
$timelines = $this->getAvailableTimelines($uid);
$timelines = [];
foreach ($this->getAvailableTimelines($uid) as $timeline) {
$timelines[] = [
'label' => $timeline->label,
'description' => $timeline->description,
'enable' => ["enable[{$timeline->code}]", '', in_array($timeline->code, $enabled_timelines)],
'bookmark' => ["bookmark[{$timeline->code}]", '', in_array($timeline->code, $bookmarked_timelines)],
];
}
$first_day_of_week = $this->pConfig->get($uid, 'calendar', 'first_day_of_week', 0);
$weekdays = [
@ -284,7 +327,13 @@ class Display extends BaseSettings
'$stay_local' => ['stay_local' , $this->t('Stay local'), $stay_local, $this->t("Don't go to a remote system when following a contact link.")],
'$preview_mode' => ['preview_mode' , $this->t('Link preview mode'), $preview_mode, $this->t('Appearance of the link preview that is added to each post with a link.'), $preview_modes, false],
'$network_timelines' => ['network_timelines[]', $this->t('Timelines for the network page:'), $network_timelines, $this->t('Select all the timelines that you want to see on your network page.'), $timelines, 'multiple'],
'$timeline_label' => $this->t('Label'),
'$timeline_descriptiom' => $this->t('Description'),
'$timeline_enable' => $this->t('Enable'),
'$timeline_bookmark' => $this->t('Bookmark'),
'$timelines' => $timelines,
'$timeline_explanation' => $this->t('Enable timelines that you want to see in the channels widget. Bookmark timelines that you want to see in the top menu.'),
'$channel_languages' => ['channel_languages[]', $this->t('Channel languages:'), $channel_languages, $this->t('Select all languages that you want to see in your channels.'), $languages, 'multiple'],
'$first_day_of_week' => ['first_day_of_week' , $this->t('Beginning of week:') , $first_day_of_week , '', $weekdays , false],
@ -292,26 +341,30 @@ class Display extends BaseSettings
]);
}
private function getAvailableTimelines(int $uid, bool $only_network = false): array
private function getAvailableTimelines(int $uid, bool $only_network = false): Timelines
{
$timelines = [];
foreach ($this->timeline->getNetworkFeeds('') as $channel) {
$timelines[$channel->code] = $this->t('%s: %s', $channel->label, $channel->description);
foreach ($this->network->getTimelines('') as $channel) {
$timelines[] = $channel;
}
if ($only_network) {
return $timelines;
return new Timelines($timelines);
}
foreach ($this->timeline->getChannelsForUser($uid) as $channel) {
$timelines[$channel->code] = $this->t('%s: %s', $channel->label, $channel->description);
foreach ($this->channel->getTimelines($uid) as $channel) {
$timelines[] = $channel;
}
foreach ($this->timeline->getCommunities(true) as $community) {
$timelines[$community->code] = $this->t('%s: %s', $community->label, $community->description);
foreach ($this->userDefinedChannel->selectByUid($uid) as $channel) {
$timelines[] = $channel;
}
return $timelines;
foreach ($this->community->getTimelines(true) as $community) {
$timelines[] = $community;
}
return new Timelines($timelines);
}
}

View file

@ -38,7 +38,7 @@ class Channel extends ChannelModule
$o = '';
if ($this->update || $this->force) {
if ($this->timeline->isChannel($this->selectedTab)) {
if ($this->channel->isTimeline($this->selectedTab) || $this->userDefinedChannel->isTimeline($this->selectedTab, $this->session->getLocalUserId())) {
$items = $this->getChannelItems();
} else {
$items = $this->getCommunityItems();

View file

@ -41,9 +41,9 @@ class Network extends NetworkModule
System::htmlUpdateExit($o);
}
if ($this->timeline->isChannel($this->selectedTab)) {
if ($this->channel->isTimeline($this->selectedTab) || $this->userDefinedChannel->isTimeline($this->selectedTab, $this->session->getLocalUserId())) {
$items = $this->getChannelItems();
} elseif ($this->timeline->isCommunity($this->selectedTab)) {
} elseif ($this->community->isTimeline($this->selectedTab)) {
$items = $this->getCommunityItems();
} else {
$items = $this->getItems();

View file

@ -156,15 +156,16 @@ class Post
/**
* Get data in a form usable by a conversation template
*
* @param array $conv_responses conversation responses
* @param string $formSecurityToken A security Token to avoid CSF attacks
* @param integer $thread_level default = 1
* @param array $conv_responses conversation responses
* @param string $formSecurityToken A security Token to avoid CSF attacks
* @param integer $thread_level default = 1
* @param array $thread_parent Array of parent guid and parent author names
*
* @return mixed The data requested on success, false on failure
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public function getTemplateData(array $conv_responses, string $formSecurityToken, int $thread_level = 1)
public function getTemplateData(array $conv_responses, string $formSecurityToken, int $thread_level = 1, array $thread_parent = [])
{
$item = $this->getData();
$edited = false;
@ -496,7 +497,12 @@ class Post
$browsershare = null;
}
$parent_guid = $thread_parent[$item['thr-parent-id']]['guid'] ?? '';
$parent_username = $thread_parent[$item['thr-parent-id']]['name'] ?? '';
$tmp_item = [
'parentguid' => $parent_guid,
'isreplyto' => DI::l10n()->t('in reply to %s', $parent_username),
'template' => $this->getTemplate(),
'type' => implode('', array_slice(explode('/', $item['verb']), -1)),
'comment_firstcollapsed' => false,
@ -609,8 +615,10 @@ class Post
$children = $this->getChildren();
$nb_children = count($children);
if ($nb_children > 0) {
$thread_parent[$item['uri-id']] = ['guid' => $item['guid'], 'name' => $item['author-name']];
foreach ($children as $child) {
$result['children'][] = $child->getTemplateData($conv_responses, $formSecurityToken, $thread_level + 1);
$thread_parent[$child->getDataValue('uri-id')] = ['guid' => $child->getDataValue('guid'), 'name' => $child->getDataValue('author-name')];
$result['children'][] = $child->getTemplateData($conv_responses, $formSecurityToken, $thread_level + 1, $thread_parent);
}
// Collapse

View file

@ -56,7 +56,7 @@ use Friendica\Database\DBA;
// This file is required several times during the test in DbaDefinition which justifies this condition
if (!defined('DB_UPDATE_VERSION')) {
define('DB_UPDATE_VERSION', 1535);
define('DB_UPDATE_VERSION', 1536);
}
return [
@ -551,6 +551,25 @@ return [
"k_expires" => ["k", "expires"],
]
],
"channel" => [
"comment" => "User defined Channels",
"fields" => [
"id" => ["type" => "int unsigned", "not null" => "1", "extra" => "auto_increment", "primary" => "1", "comment" => ""],
"uid" => ["type" => "mediumint unsigned", "not null" => "1", "foreign" => ["user" => "uid"], "comment" => "User id"],
"label" => ["type" => "varchar(64)", "not null" => "1", "comment" => "Channel label"],
"description" => ["type" => "varchar(64)", "comment" => "Channel description"],
"circle" => ["type" => "int", "comment" => "Circle or channel that this channel is based on"],
"access-key" => ["type" => "varchar(1)", "comment" => "Access key"],
"include-tags" => ["type" => "varchar(255)", "comment" => "Comma separated list of tags that will be included in the channel"],
"exclude-tags" => ["type" => "varchar(255)", "comment" => "Comma separated list of tags that aren't allowed in the channel"],
"full-text-search" => ["type" => "varchar(255)", "comment" => "Full text search pattern, see https://mariadb.com/kb/en/full-text-index-overview/#in-boolean-mode"],
"media-type" => ["type" => "smallint unsigned", "comment" => "Filtered media types"],
],
"indexes" => [
"PRIMARY" => ["id"],
"uid" => ["uid"],
]
],
"config" => [
"comment" => "main configuration storage",
"fields" => [
@ -1332,6 +1351,7 @@ return [
"contact-type" => ["type" => "tinyint", "not null" => "1", "default" => "0", "comment" => "Person, organisation, news, community, relay"],
"media-type" => ["type" => "tinyint", "not null" => "1", "default" => "0", "comment" => "Type of media in a bit array (1 = image, 2 = video, 4 = audio"],
"language" => ["type" => "varbinary(128)", "comment" => "Language information about this post"],
"searchtext" => ["type" => "mediumtext", "comment" => "Simplified text for the full text search"],
"created" => ["type" => "datetime", "comment" => ""],
"restricted" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "If true, this post is either unlisted or not from a federated network"],
"comments" => ["type" => "mediumint unsigned", "comment" => "Number of comments"],
@ -1341,6 +1361,7 @@ return [
"PRIMARY" => ["uri-id"],
"owner-id" => ["owner-id"],
"created" => ["created"],
"searchtext" => ["FULLTEXT", "searchtext"],
]
],
"post-history" => [

View file

@ -184,6 +184,7 @@
"author-blocked" => ["author", "blocked"],
"author-hidden" => ["author", "hidden"],
"author-updated" => ["author", "updated"],
"author-contact-type" => ["author", "contact-type"],
"author-gsid" => ["author", "gsid"],
"author-baseurl" => ["author", "baseurl"],
"owner-id" => ["post-user", "owner-id"],
@ -366,6 +367,7 @@
"author-blocked" => ["author", "blocked"],
"author-hidden" => ["author", "hidden"],
"author-updated" => ["author", "updated"],
"author-contact-type" => ["author", "contact-type"],
"author-gsid" => ["author", "gsid"],
"owner-id" => ["post-thread-user", "owner-id"],
"owner-uri-id" => ["owner", "uri-id"],
@ -532,6 +534,7 @@
"author-blocked" => ["author", "blocked"],
"author-hidden" => ["author", "hidden"],
"author-updated" => ["author", "updated"],
"author-contact-type" => ["author", "contact-type"],
"author-gsid" => ["author", "gsid"],
"owner-id" => ["post", "owner-id"],
"owner-uri-id" => ["owner", "uri-id"],
@ -675,6 +678,7 @@
"author-blocked" => ["author", "blocked"],
"author-hidden" => ["author", "hidden"],
"author-updated" => ["author", "updated"],
"author-contact-type" => ["author", "contact-type"],
"author-gsid" => ["author", "gsid"],
"owner-id" => ["post-thread", "owner-id"],
"owner-uri-id" => ["owner", "uri-id"],

View file

@ -798,9 +798,13 @@ return [
],
'channel' => [
// engagement_hours (Integer)
// Number of hours posts are held in the engagement table
// Maximum age of incoming posts for the engagement table, when the engagement post limit is 0 or hasn't been reached yet.
'engagement_hours' => 24,
// engagement_post_limit (Integer)
// NUmber of posts that are held in the engagement table
'engagement_post_limit' => 20000,
// interaction_score_days (Integer)
// Number of days that are used to calculate the interaction score.
'interaction_score_days' => 30,

View file

@ -651,6 +651,7 @@ return [
'/{open}' => [Module\Settings\Account::class, [R::GET, R::POST]],
],
'/addons[/{addon}]' => [Module\Settings\Addons::class, [R::GET, R::POST]],
'/channels' => [Module\Settings\Channels::class, [R::GET, R::POST]],
'/connectors[/{connector}]' => [Module\Settings\Connectors::class, [R::GET, R::POST]],
'/delegation[/{action}/{user_id}]' => [Module\Settings\Delegation::class, [R::GET, R::POST]],
'/display' => [Module\Settings\Display::class, [R::GET, R::POST]],

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,45 @@
<div class="generic-page-wrapper">
<h1>{{$l10n.title}}</h1>
<p>{{$l10n.intro}}</p>
<h2>{{$l10n.addtitle}}</h2>
<form action="{{$baseurl}}/settings/channels" method="post">
<input type="hidden" name="form_security_token" value="{{$form_security_token}}">
{{include file="field_input.tpl" field=$label}}
{{include file="field_input.tpl" field=$description}}
{{include file="field_input.tpl" field=$access_key}}
{{include file="field_select.tpl" field=$circle}}
{{include file="field_input.tpl" field=$include_tags}}
{{include file="field_input.tpl" field=$exclude_tags}}
{{include file="field_input.tpl" field=$text_search}}
{{include file="field_checkbox.tpl" field=$image}}
{{include file="field_checkbox.tpl" field=$video}}
{{include file="field_checkbox.tpl" field=$audio}}
<div class="submit">
<button type="submit" class="btn btn-primary" name="add_channel" value="{{$l10n.addsubmit}}">{{$l10n.addsubmit}}</button>
</div>
</form>
{{if $entries}}
<h2>{{$l10n.currenttitle}}</h2>
<form action="{{$baseurl}}/settings/channels" method="post">
<input type="hidden" name="form_security_token" value="{{$form_security_token}}">
{{foreach $entries as $e}}
{{include file="field_input.tpl" field=$e.label}}
{{include file="field_input.tpl" field=$e.description}}
{{include file="field_input.tpl" field=$e.access_key}}
{{include file="field_select.tpl" field=$e.circle}}
{{include file="field_input.tpl" field=$e.include_tags}}
{{include file="field_input.tpl" field=$e.exclude_tags}}
{{include file="field_input.tpl" field=$e.text_search}}
{{include file="field_checkbox.tpl" field=$e.image}}
{{include file="field_checkbox.tpl" field=$e.video}}
{{include file="field_checkbox.tpl" field=$e.audio}}
{{include file="field_checkbox.tpl" field=$e.delete}}
<hr>
{{/foreach}}
<div class="submit">
<button type="submit" class="btn btn-primary" name="edit_channel" value="{{$l10n.savechanges}}">{{$l10n.savechanges}}</button>
</div>
{{/if}}
</form>
</div>

View file

@ -22,7 +22,27 @@
{{include file="field_select.tpl" field=$preview_mode}}
<h2>{{$timeline_title}}</h2>
{{include file="field_select.tpl" field=$network_timelines}}
{{$timeline_explanation}}
<table class="table table-condensed table-striped table-bordered">
<thead>
<tr>
<th>{{$timeline_label}}</th>
<th>{{$timeline_descriptiom}}</th>
<th>{{$timeline_enable}}</th>
<th>{{$timeline_bookmark}}</th>
</tr>
</thead>
<tbody>
{{foreach $timelines as $t}}
<tr>
<td>{{$t.label}}</td>
<td>{{$t.description}}</td>
<td>{{include file="field_checkbox.tpl" field=$t.enable}}</td>
<td>{{include file="field_checkbox.tpl" field=$t.bookmark}}</td>
</tr>
{{/foreach}}
</tbody>
</table>
<h2>{{$channel_title}}</h2>
{{include file="field_select.tpl" field=$channel_languages}}

View file

@ -72,9 +72,16 @@
{{/if}}
{{if $profile.addr}}
<div id="mention-link-button">
<button type="button" id="mention-link" class="btn btn-labeled btn-primary" onclick="openWallMessage('compose/0?body={{if $profile.forum==1}}!{{else}}@{{/if}}{{$profile.addr}}')">
<button type="button" id="mention-link" class="btn btn-labeled btn-primary" onclick="openWallMessage('{{$mention_url}}')">
<span class=""><i class="fa fa-pencil-square-o"></i></span>
<span class="">{{$mention}}</span>
<span class="">{{$mention_label}}</span>
</div>
{{/if}}
{{if $network_label}}
<div id="showgroup-button">
<button type="button" id="showgroup" class="btn btn-labeled btn-primary" onclick="window.location.href=('{{$network_url}}')">
<span class=""><i class="fa fa-group"></i></span>
<span class="">{{$network_label}}</span>
</div>
{{/if}}
</div>

View file

@ -84,7 +84,27 @@
</div>
<div id="timeline-settings-content" class="panel-collapse collapse{{if !$theme && !$mobile_theme && !$theme_config}} in{{/if}}" role="tabpanel" aria-labelledby="timeline-settings">
<div class="panel-body">
{{include file="field_select.tpl" field=$network_timelines}}
{{$timeline_explanation}}
<table class="table table-condensed table-striped table-bordered">
<thead>
<tr>
<th>{{$timeline_label}}</th>
<th>{{$timeline_descriptiom}}</th>
<th>{{$timeline_enable}}</th>
<th>{{$timeline_bookmark}}</th>
</tr>
</thead>
<tbody>
{{foreach $timelines as $t}}
<tr>
<td>{{$t.label}}</td>
<td>{{$t.description}}</td>
<td>{{include file="field_checkbox.tpl" field=$t.enable}}</td>
<td>{{include file="field_checkbox.tpl" field=$t.bookmark}}</td>
</tr>
{{/foreach}}
</tbody>
</table>
</div>
<div class="panel-footer">
<button type="submit" name="submit" class="btn btn-primary" value="{{$submit}}">{{$submit}}</button>

View file

@ -59,9 +59,15 @@ as the value of $top_child_total (this is done at the end of this file)
<span class="uriid" style="display: none;">{{$item.uriid}}</span>
{{/if}}
<div class="media {{$item.shiny}}">
{{if $item.reshared}}
<p class="wall-item-announce wall-item-responses" id="wall-item-announce-{{$item.id}}"><i class="fa fa-retweet" aria-hidden="true"></i> {{$item.reshared nofilter}}</p>
{{if $item.parentguid}}
<a id="btn-{{$item.id}}" class="time" href="javascript:;" onclick="scrollToItem('item-' + '{{$item.parentguid}}');">{{$item.isreplyto}}</a>
{{if $item.reshared}}<span class="hidden-xs">&#x2022;</span>{{/if}}
<br class="visible-xs">
{{/if}}
{{if $item.reshared}}
<span class="wall-item-announce wall-item-responses" id="wall-item-announce-{{$item.id}}"><i class="fa fa-retweet" aria-hidden="true"></i> {{$item.reshared nofilter}}</span>
{{/if}}
<p>
{{* The avatar picture and the photo-menu *}}
<div class="dropdown pull-left"><!-- Dropdown -->
{{if $item.thread_level==1}}

View file

@ -58,17 +58,17 @@
{{/if}}
{{if $contact.addr}}
<div id="mention-link-button">
<button type="button" id="mention-link" class="btn btn-labeled btn-primary{{if !$always_open_compose}} modal-open{{/if}}" onclick="openWallMessage('compose/0?body={{if $contact.forum==1}}!{{else}}@{{/if}}{{$contact.addr}}')">
<button type="button" id="mention-link" class="btn btn-labeled btn-primary{{if !$always_open_compose}} modal-open{{/if}}" onclick="openWallMessage('{{$mention.1}}')" titel="{{$mention.0}}" aria-label="{{$mention.0}}">
<span class=""><i class="fa fa-pencil-square-o"></i></span>
<span class="">{{if $contact.forum==1}}{{$posttogroup}}{{else}}{{$mention}}{{/if}}</span>
<span class="">{{$mention.0}}</span>
</button>
</div>
{{/if}}
{{if $contact.forum==1 || $contact.prv==1}}
<div id="show-group-button">
<button type="button" id="show-group" class="btn btn-labeled btn-primary{{if !$always_open_compose}} modal-open{{/if}}" onclick="window.location.href='{{$showgroup_link}}'">
<button type="button" id="show-group" class="btn btn-labeled btn-primary{{if !$always_open_compose}} modal-open{{/if}}" onclick="window.location.href='{{$showgroup.1}}'">
<span class=""><i class="fa fa-group"></i></span>
<span class="">{{$showgroup}}</span>
<span class="">{{$showgroup.0}}</span>
</button>
</div>
{{/if}}