Added support for user cover photos (work in progress)
authorAlexander Ebert <ebert@woltlab.com>
Tue, 28 Nov 2017 11:36:28 +0000 (12:36 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Tue, 28 Nov 2017 11:36:37 +0000 (12:36 +0100)
See #2484

12 files changed:
com.woltlab.wcf/option.xml
com.woltlab.wcf/templates/user.tpl
com.woltlab.wcf/userGroupOption.xml
constants.php
wcfsetup/install/files/lib/data/user/User.class.php
wcfsetup/install/files/lib/data/user/UserProfile.class.php
wcfsetup/install/files/lib/data/user/cover/photo/DefaultUserCoverPhoto.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/user/cover/photo/IUserCoverPhoto.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/user/cover/photo/UserCoverPhoto.class.php [new file with mode: 0644]
wcfsetup/install/files/style/ui/userProfile.scss
wcfsetup/install/lang/de.xml
wcfsetup/install/lang/en.xml

index 2a87e5b915941d6df0c7376b4a1f1394e9a2efa3..0794ebb5b85a76c31c6e7f2752adf4d48d519fb0 100644 (file)
                                <optiontype>boolean</optiontype>
                                <defaultvalue>1</defaultvalue>
                        </option>
+                       <option name="module_user_cover_photo">
+                               <categoryname>module.user</categoryname>
+                               <optiontype>boolean</optiontype>
+                               <defaultvalue>1</defaultvalue>
+                       </option>
                        
                        <option name="module_like">
                                <categoryname>module.community</categoryname>
index b8e45fb4b36e668526aecb03108a9badc16346eb..df335ebee2e24374de3c09fbf8082c9a260ce916 100644 (file)
 {/capture}
 
 {capture assign='contentHeader'}
-       <header class="contentHeader userProfileUser"{if $isAccessible}
+       <header class="contentHeader userProfileUser{if MODULE_USER_COVER_PHOTO} userProfileUserWithCoverPhoto{/if}"{if $isAccessible}
                data-object-id="{@$user->userID}"
                {if $__wcf->session->getPermission('admin.user.canBanUser')}
                        data-banned="{@$user->banned}"
                        data-is-disabled="{if $user->activationCode}true{else}false{/if}"
                {/if}
                {/if}>
+               {if MODULE_USER_COVER_PHOTO}
+                       <div class="userProfileCoverPhoto" style="background-image: url({$user->getCoverPhoto()->getURL()})">
+                               {if $user->userID == $__wcf->user->userID}<a href="#" class="button small jsButtonEditCoverPhoto"><span class="icon icon16 fa-pencil"></span> {lang}wcf.user.coverPhoto.edit{/lang}</a>{/if}
+                       </div>
+               {/if}
                <div class="contentHeaderIcon">
                        {if $user->userID == $__wcf->user->userID}
                                <a href="{link controller='AvatarEdit'}{/link}" class="jsTooltip" title="{lang}wcf.user.avatar.edit{/lang}">{@$user->getAvatar()->getImageTag(128)}</a>
                
                <div class="contentHeaderTitle">
                        <h1 class="contentTitle">
-                               {$user->username}
+                               <span class="userProfileUsername">{if $user->username === 'root'}Alexander Ebert{else}{$user->username}{/if}</span>
                                {if $user->banned}<span class="icon icon24 fa-lock jsTooltip jsUserBanned" title="{lang}wcf.user.banned{/lang}"></span>{/if}
                                {if MODULE_USER_RANK}
                                        {if $user->getUserTitle()}
index 8f8a292aa1d4240b1bdbd75aa5538a555a12276f..d36f53aaa9b5cab89f7fc4bbe9b44bb6859644c1 100644 (file)
@@ -15,6 +15,9 @@
                        <category name="user.profile.avatar">
                                <parent>user.profile</parent>
                        </category>
+                       <category name="user.profile.coverPhoto">
+                               <parent>user.profile</parent>
+                       </category>
                        <category name="user.profile.trophy">
                                <parent>user.profile</parent>
                        </category>
@@ -734,6 +737,19 @@ jpeg
 png</defaultvalue>
                                <usersonly>1</usersonly>
                        </option>
+                       
+                       <option name="user.profile.coverPhoto.canSeeCoverPhotos">
+                               <categoryname>user.profile.coverPhoto</categoryname>
+                               <optiontype>boolean</optiontype>
+                               <defaultvalue>1</defaultvalue>
+                       </option>
+                       <option name="user.profile.coverPhoto.canUploadCoverPhoto">
+                               <categoryname>user.profile.coverPhoto</categoryname>
+                               <optiontype>boolean</optiontype>
+                               <defaultvalue>1</defaultvalue>
+                               <usersonly>1</usersonly>
+                       </option>
+                       
                        <option name="user.profile.trophy.canSeeTrophies">
                                <categoryname>user.profile.trophy</categoryname>
                                <optiontype>boolean</optiontype>
index e9057c74d8759aed26d8fb243f8676595c76627a..1649610aacc226c367b460f2c8ac0ad384d3b042 100644 (file)
@@ -236,3 +236,4 @@ define('ENABLE_DEVELOPER_TOOLS', 0);
 define('FORCE_LOGIN', 0);
 define('DESKTOP_NOTIFICATION_PACKAGE_ID', 1);
 define('PAGE_LOGO_LINK_TO_APP_DEFAULT', 1);
+define('MODULE_USER_COVER_PHOTO', 1);
index a66ded5a2ae4e41289560ce9d113b2be286af8eb..9f4e4349c3388de7b6868d34b0a91e1544dd11ed 100644 (file)
@@ -64,6 +64,10 @@ use wcf\util\UserUtil;
  * @property-read      string          $notificationMailToken          token used for authenticating requests by the user to disable notification emails
  * @property-read      string          $authData                       data of the third party used for authentication
  * @property-read      integer         $likesReceived                  cumulative result of likes (counting +1) and dislikes (counting -1) the user's contents have received
+ * @property-read       string          $coverPhotoHash                 hash of the user's cover photo
+ * @property-read      string          $coverPhotoExtension            extension of the user's cover photo file
+ * @property-read       integer         $disableCoverPhoto              is `1` if the user's cover photo has been disabled, otherwise `0`
+ * @property-read      integer         $disableCoverPhotoExpires       timestamp at which the user's cover photo will automatically be enabled again
  */
 final class User extends DatabaseObject implements IRouteController, IUserContent {
        /**
index c1a67328595030664b6da6d5a5a8d1b563019a72..a56ea4fb9722b738d9af8ba82673aff93566ad2c 100644 (file)
@@ -6,6 +6,9 @@ use wcf\data\user\avatar\DefaultAvatar;
 use wcf\data\user\avatar\Gravatar;
 use wcf\data\user\avatar\IUserAvatar;
 use wcf\data\user\avatar\UserAvatar;
+use wcf\data\user\cover\photo\DefaultUserCoverPhoto;
+use wcf\data\user\cover\photo\IUserCoverPhoto;
+use wcf\data\user\cover\photo\UserCoverPhoto;
 use wcf\data\user\group\UserGroup;
 use wcf\data\user\online\UserOnline;
 use wcf\data\user\option\ViewableUserOption;
@@ -43,49 +46,55 @@ class UserProfile extends DatabaseObjectDecorator implements ITitledLinkObject {
         * list of ignored user ids
         * @var integer[]
         */
-       protected $ignoredUserIDs = null;
+       protected $ignoredUserIDs;
        
        /**
         * list of follower user ids
         * @var integer[]
         */
-       protected $followerUserIDs = null;
+       protected $followerUserIDs;
        
        /**
         * list of following user ids
         * @var integer[]
         */
-       protected $followingUserIDs = null;
+       protected $followingUserIDs;
        
        /**
         * user avatar
         * @var IUserAvatar
         */
-       protected $avatar = null;
+       protected $avatar;
        
        /**
         * user rank object
         * @var UserRank
         */
-       protected $rank = null;
+       protected $rank;
        
        /**
         * age of this user
         * @var integer
         */
-       protected $__age = null;
+       protected $__age;
        
        /**
         * group data and permissions
         * @var mixed[][]
         */
-       protected $groupData = null;
+       protected $groupData;
        
        /**
         * current location of this user.
         * @var string
         */
-       protected $currentLocation = null;
+       protected $currentLocation;
+       
+       /**
+        * user cover photo
+        * @var UserCoverPhoto
+        */
+       protected $coverPhoto;
        
        const GENDER_MALE = 1;
        const GENDER_FEMALE = 2;
@@ -282,6 +291,37 @@ class UserProfile extends DatabaseObjectDecorator implements ITitledLinkObject {
                return (WCF::getUser()->userID == $this->userID || WCF::getSession()->getPermission('user.profile.avatar.canSeeAvatars'));
        }
        
+       /**
+        * Returns the user's cover photo.
+        * 
+        * @return      IUserCoverPhoto
+        */
+       public function getCoverPhoto() {
+               if ($this->coverPhoto === null) {
+                       if (!$this->disableCoverPhoto && $this->coverPhotoHash) {
+                               if ($this->canSeeCoverPhoto()) {
+                                       $this->coverPhoto = new UserCoverPhoto($this->userID, $this->coverPhotoHash, $this->coverPhotoExtension);
+                               }
+                       }
+                       
+                       // use default cover photo
+                       if ($this->coverPhoto === null) {
+                               $this->coverPhoto = new DefaultUserCoverPhoto();
+                       }
+               }
+               
+               return $this->coverPhoto;
+       }
+       
+       /**
+        * Returns true if the active user can view the cover photo of this user.
+        *
+        * @return      boolean
+        */
+       public function canSeeCoverPhoto() {
+               return (WCF::getUser()->userID == $this->userID || WCF::getSession()->getPermission('user.profile.coverPhoto.canSeeCoverPhotos'));
+       }
+       
        /**
         * Returns true if this user is currently online.
         * 
diff --git a/wcfsetup/install/files/lib/data/user/cover/photo/DefaultUserCoverPhoto.class.php b/wcfsetup/install/files/lib/data/user/cover/photo/DefaultUserCoverPhoto.class.php
new file mode 100644 (file)
index 0000000..318bf19
--- /dev/null
@@ -0,0 +1,39 @@
+<?php
+namespace wcf\data\user\cover\photo;
+use wcf\system\style\StyleHandler;
+use wcf\system\WCF;
+
+/**
+ * Represents a default cover photo.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2017 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\Data\User\Cover\Photo
+ */
+class DefaultUserCoverPhoto implements IUserCoverPhoto {
+       /**
+        * @inheritDoc
+        */
+       public function getLocation() {
+               return WCF_DIR . 'images/coverPhotos/' . $this->getFilename();
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function getURL() {
+               return WCF::getPath() . 'images/coverPhotos/' . $this->getFilename();
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function getFilename() {
+               /*if ($coverPhoto = StyleHandler::getInstance()->getStyle()->getDefaultCoverPhoto()) {
+                       return $coverPhoto;
+               }*/
+               
+               return 'default.png';
+       }
+}
diff --git a/wcfsetup/install/files/lib/data/user/cover/photo/IUserCoverPhoto.class.php b/wcfsetup/install/files/lib/data/user/cover/photo/IUserCoverPhoto.class.php
new file mode 100644 (file)
index 0000000..4a207f8
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+namespace wcf\data\user\cover\photo;
+
+/**
+ * Any displayable cover photo type should implement this class.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2017 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\Data\User\Cover\Photo
+ */
+interface IUserCoverPhoto {
+       
+       /**
+        * Returns the physical location of this cover photo.
+        *
+        * @return      string
+        */
+       public function getLocation();
+       
+       /**
+        * Returns the url to this cover photo.
+        *
+        * @return      string
+        */
+       public function getURL();
+       
+       /**
+        * Returns the file name of this cover photo.
+        *
+        * @return      string
+        */
+       public function getFilename();
+}
diff --git a/wcfsetup/install/files/lib/data/user/cover/photo/UserCoverPhoto.class.php b/wcfsetup/install/files/lib/data/user/cover/photo/UserCoverPhoto.class.php
new file mode 100644 (file)
index 0000000..d7b6868
--- /dev/null
@@ -0,0 +1,70 @@
+<?php
+namespace wcf\data\user\cover\photo;
+use wcf\system\WCF;
+
+/**
+ * Represents a user's cover photo.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2017 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\Data\User\Cover\Photo
+ */
+class UserCoverPhoto implements IUserCoverPhoto {
+       /**
+        * file extension
+        * @var string
+        */
+       protected $coverPhotoExtension;
+       
+       /**
+        * file hash
+        * @var string
+        */
+       protected $coverPhotoHash;
+       
+       /**
+        * user id
+        * @var integer
+        */
+       protected $userID;
+       
+       const MAX_HEIGHT = 400;
+       const MAX_WIDTH = 1600;
+       const MIN_HEIGHT = 200;
+       const MIN_WIDTH = 800;
+       
+       /**
+        * UserCoverPhoto constructor.
+        * 
+        * @param       integer         $userID
+        * @param       string          $coverPhotoHash
+        * @param       string          $coverPhotoExtension
+        */
+       public function __construct($userID, $coverPhotoHash, $coverPhotoExtension) {
+               $this->userID = $userID;
+               $this->coverPhotoHash = $coverPhotoHash;
+               $this->coverPhotoExtension = $coverPhotoExtension;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function getLocation() {
+               return WCF_DIR . 'images/coverPhotos/' . $this->getFilename();
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function getURL() {
+               return WCF::getPath() . 'images/coverPhotos/' . $this->getFilename();
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function getFilename() {
+               return substr($this->coverPhotoHash, 0, 2) . '/' . $this->userID . '-' . $this->coverPhotoHash . '.' . $this->coverPhotoExtension;
+       }
+}
index ae4e9382551e506528ccaeadefa708a2bb909ce8..d4fde7af557eaaaf6fceb88b96cbf6c504b4f30c 100644 (file)
                display: flex;
                flex-wrap: wrap;
                
+               .contentHeaderNavigation {
+                       flex: 0 0 100%;
+               }
+       }
+       
+       @include screen-sm {
+               .contentHeaderIcon {
+                       display: block;
+                       flex: 0 0 96px;
+                       margin-right: 15px;
+                       
+                       img {
+                               width: 96px !important;
+                               height: 96px !important;
+                       }
+               }
+               
+               .contentHeaderTitle {
+                       flex: 0 0 calc(100% - 111px);
+                       max-width: calc(100% - 11px);
+               }
+       }
+       
+       @include screen-xs {
                .contentHeaderIcon {
                        display: block;
                        flex: 0 0 48px;
                        max-width: calc(100% - 58px);
                }
                
-               .contentHeaderNavigation {
-                       flex: 0 0 100%;
-               }
-       }
-       
-       @include screen-xs {
                .contentHeaderNavigation {
                        .userProfileButtonContainer {
                                display: flex;
        }
 }
 
+/* user profile cover photo */
+.userProfileUserWithCoverPhoto {
+       padding-top: 165px;
+       position: relative;
+       
+       .userProfileCoverPhoto {
+               background: no-repeat center;
+               background-size: cover;
+               border-radius: 3px;
+               height: 200px;
+               left: 0;
+               position: absolute;
+               right: 0;
+               top: 0;
+               
+               &::after {
+                       background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0) 70%, rgba(0, 0, 0, .5) 100%);
+                       border-bottom-left-radius: 3px;
+                       border-bottom-right-radius: 3px;
+                       bottom: 0;
+                       content: "";
+                       display: block;
+                       left: 0;
+                       pointer-events: none;
+                       position: absolute;
+                       right: 0;
+                       top: 0;
+               }
+               
+               .jsButtonEditCoverPhoto {
+                       position: absolute;
+                       right: 10px;
+                       top: 10px;
+               }
+       }
+       
+       .contentHeaderTitle {
+               /* avoid being covered by the photo */
+               z-index: 10;
+       }
+       
+       .userProfileUsername {
+               color: #fff;
+               text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.6);
+       }
+       
+       @include screen-md-up {
+               .contentHeaderIcon {
+                       flex: 0 0 138px;
+                       margin-top: -29px; /* 35px photo overlap - (128px height / 2) */
+                       padding-left: 10px;
+               }
+               
+               .contentHeaderDescription {
+                       margin-top: 15px !important;
+               }
+               
+               .contentHeaderNavigation {
+                       padding-top: 45px;
+               }
+       }
+       
+       @include screen-sm-up {
+               .contentHeaderIcon .badgeOnline {
+                       left: 10px !important;
+               }
+               
+               .userProfileUsername + .badge {
+                       margin-left: 5px;
+               }
+       }
+       
+       @include screen-sm {
+               padding-top: 170px;
+               
+               .contentHeaderIcon {
+                       margin-top: -18px; /* 30px photo overlap - (96px height / 2) */
+                       padding-left: 5px;
+               }
+               
+               .contentHeaderDescription {
+                       margin-top: 10px !important;
+               }
+       }
+       
+       @include screen-xs {
+               padding-top: 120px;
+               
+               .userProfileCoverPhoto {
+                       height: 150px;
+               }
+               
+               .contentHeaderIcon {
+                       margin-top: 6px; /* 30px photo overlap - (48px height / 2) */
+                       padding-left: 5px;
+               }
+               
+               .contentTitle {
+                       margin-bottom: 35px;
+                       position: relative;
+               }
+               
+               .userProfileUsername {
+                       display: block;
+               }
+               
+               .userProfileUsername + .badge {
+                       margin-left: 0;
+                       position: absolute;
+                       top: 35px !important;
+               }
+               
+               .contentHeaderDescription {
+                       margin-left: -58px;
+               }
+       }
+}
+
 .userTitleBadge {
        max-width: 154px;
        overflow: hidden;
index 54ea8628b1ad9a4225ad8d10bd6270b96c3a8d2f..a4776ce42287daa030a1aad4dd647143847b00d6 100644 (file)
@@ -3927,6 +3927,10 @@ Die E-Mail-Adresse des neuen Benutzers lautet: {@$user->email}
                <item name="wcf.user.condition.trophyPoints"><![CDATA[Trophäen]]></item>
        </category>
        
+       <category name="wcf.user.coverPhoto">
+               <item name="wcf.user.coverPhoto.edit"><![CDATA[Profilbild bearbeiten]]></item>
+       </category>
+       
        <category name="wcf.user.notification">
                <item name="wcf.user.notification.button.confirmed"><![CDATA[OK]]></item>
                <item name="wcf.user.notification.count"><![CDATA[if (data.returnValues.count == 0) { "Keine Benachrichtigungen" } else if (data.returnValues.count == 1) { "Eine Benachrichtigung" } else { data.returnValues.count + " Benachrichtigungen" }]]></item>
index e5c8c94fcc0d49498318d42c3ea1db3ae776a5b2..e1df272b26cd1208a3d5b0b760d5368f2da2a309 100644 (file)
@@ -3919,6 +3919,10 @@ Open the link below to access the user profile:
                <item name="wcf.user.condition.trophyPoints"><![CDATA[Trophies]]></item>
        </category>
        
+       <category name="wcf.user.coverPhoto">
+               <item name="wcf.user.coverPhoto.edit"><![CDATA[Edit Cover Photo]]></item>
+       </category>
+       
        <category name="wcf.user.notification">
                <item name="wcf.user.notification.button.confirmed"><![CDATA[OK]]></item>
                <item name="wcf.user.notification.count"><![CDATA[if (data.returnValues.count == 0) { "No Notifications" } else if (data.returnValues.count == 1) { "1 Notification" } else { data.returnValues.count + " Notifications" }]]></item>