diff --git a/README.md b/README.md index 3b4d404..9dcf1f0 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,10 @@ php flarum cache:clear # Features * Tracks and displays **unique** profileviews in the usercard +* Displays a list of last viewers on a user profile # Media ![image](http://puu.sh/yxd7o.png) + +![image](http://puu.sh/CCJvo.png) \ No newline at end of file diff --git a/composer.json b/composer.json index b42ed12..3acda6d 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ }, "autoload": { "psr-4": { - "michaelbelgium\\profileviews\\": "src/" + "Michaelbelgium\\Profileviews\\": "src/" } }, "extra": { diff --git a/extend.php b/extend.php index 35766f4..b598c4e 100644 --- a/extend.php +++ b/extend.php @@ -1,26 +1,22 @@ js(__DIR__. '/js/dist/forum.js'), + ->js(__DIR__. '/js/dist/forum.js') + ->css(__DIR__. '/less/extension.less'), new Locales(__DIR__ . '/locale'), + (new Routes('api')) + ->post('/profileview', 'profileview.create', CreateUserProfileViewController::class), + function (Dispatcher $events) { - $events->subscribe(listeners\AddProfileViewHandler::class); - $events->subscribe(listeners\AddUserProfileViewsRelationship::class); - - $events->listen(Serializing::class, function (Serializing $event) { - if ($event->isSerializer(UserSerializer::class)) { - $event->attributes['views'] = $event->model->profileViews()->count(); - } - }); + $events->subscribe(AddUserProfileViewsRelationship::class); } ]; \ No newline at end of file diff --git a/js/dist/forum.js b/js/dist/forum.js index 2d23c6a..1c8a55f 100644 --- a/js/dist/forum.js +++ b/js/dist/forum.js @@ -1,2 +1,2 @@ -module.exports=function(e){var t={};function r(o){if(t[o])return t[o].exports;var n=t[o]={i:o,l:!1,exports:{}};return e[o].call(n.exports,n,n.exports,r),n.l=!0,n.exports}return r.m=e,r.c=t,r.d=function(e,t,o){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:o})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(r.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var n in e)r.d(o,n,function(t){return e[t]}.bind(null,n));return o},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=6)}([function(e,t){e.exports=flarum.core.compat.app},function(e,t){e.exports=flarum.core.compat["models/User"]},function(e,t){e.exports=flarum.core.compat["components/UserCard"]},function(e,t){e.exports=flarum.core.compat["helpers/icon"]},function(e,t){e.exports=flarum.core.compat.Model},function(e,t){e.exports=flarum.core.compat.extend},function(e,t,r){"use strict";r.r(t);var o=r(0),n=r.n(o),u=r(1),i=r.n(u),a=r(2),c=r.n(a),f=r(3),l=r.n(f),p=r(4),s=r.n(p),d=r(5);n.a.initializers.add("michaelbelgium-flarum-profile-views",function(){i.a.prototype.views=s.a.attribute("views"),Object(d.extend)(c.a.prototype,"infoItems",function(e){var t=this.props.user;e.add("profile-views",m("span",null,l()("far fa-eye")," ",n.a.translator.trans("flarum_profile_views.forum.user.views_count_text",{viewcount:0==t.views()?"0":t.views()})))})})}]); +module.exports=function(e){var r={};function t(o){if(r[o])return r[o].exports;var n=r[o]={i:o,l:!1,exports:{}};return e[o].call(n.exports,n,n.exports,t),n.l=!0,n.exports}return t.m=e,t.c=r,t.d=function(e,r,o){t.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:o})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,r){if(1&r&&(e=t(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(t.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)t.d(o,n,function(r){return e[r]}.bind(null,n));return o},t.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(r,"a",r),r},t.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},t.p="",t(t.s=11)}([function(e,r){e.exports=flarum.core.compat.Model},function(e,r){e.exports=flarum.core.compat.extend},function(e,r){e.exports=flarum.core.compat["components/UserPage"]},function(e,r){e.exports=flarum.core.compat["models/User"]},function(e,r){e.exports=flarum.core.compat["components/UserCard"]},function(e,r){e.exports=flarum.core.compat["components/FieldSet"]},function(e,r){e.exports=flarum.core.compat["helpers/icon"]},function(e,r){e.exports=flarum.core.compat["helpers/avatar"]},function(e,r){e.exports=flarum.core.compat["helpers/username"]},function(e,r){e.exports=flarum.core.compat["utils/ItemList"]},function(e,r){e.exports=flarum.core.compat["utils/humanTime"]},function(e,r,t){"use strict";t.r(r);var o=t(3),n=t.n(o),a=t(2),i=t.n(a),s=t(4),u=t.n(s),p=t(5),l=t.n(p),c=t(6),f=t.n(c),d=t(7),v=t.n(d),w=t(8),b=t.n(w),h=t(0),y=t.n(h),x=t(9),_=t.n(x),O=t(1),g=t(10),U=t.n(g);function j(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}function P(e,r,t){return r in e?Object.defineProperty(e,r,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[r]=t,e}var S=function(e){var r,t;function o(){for(var r,t=arguments.length,o=new Array(t),n=0;n=5&&(t=t.slice(0,5)),t.forEach(function(e){r.add("lastUser-"+e.viewer().id(),m("a",{href:app.forum.attribute("baseUrl")+"/u/"+e.viewer().username()},v()(e.viewer(),{className:"lastUser-avatar"}),m("div",null,b()(e.viewer()),m("span",{className:"lastUser-visited",title:e.visitedAt().toLocaleString()},U()(e.visitedAt())))))})),e.add("lastViewedUsers",l.a.component({label:app.translator.trans("flarum_profile_views.forum.user.title_last_viewers"),className:"LastUsers",children:r.toArray()}))}),Object(O.extend)(i.a.prototype,"show",function(){void 0!==app.session.user&&app.session.user.id()!==this.user.id()&&app.request({method:"POST",url:app.forum.attribute("apiUrl")+"/profileview",data:{viewer:app.session.user.id(),viewedUser:this.user.id()}})})})}]); //# sourceMappingURL=forum.js.map \ No newline at end of file diff --git a/js/dist/forum.js.map b/js/dist/forum.js.map index 2f816bc..b0a3888 100644 --- a/js/dist/forum.js.map +++ b/js/dist/forum.js.map @@ -1 +1 @@ -{"version":3,"sources":["webpack://@michaelbelgium/flarum-profile-views/webpack/bootstrap","webpack://@michaelbelgium/flarum-profile-views/external \"flarum.core.compat['app']\"","webpack://@michaelbelgium/flarum-profile-views/external \"flarum.core.compat['models/User']\"","webpack://@michaelbelgium/flarum-profile-views/external \"flarum.core.compat['components/UserCard']\"","webpack://@michaelbelgium/flarum-profile-views/external \"flarum.core.compat['helpers/icon']\"","webpack://@michaelbelgium/flarum-profile-views/external \"flarum.core.compat['Model']\"","webpack://@michaelbelgium/flarum-profile-views/external \"flarum.core.compat['extend']\"","webpack://@michaelbelgium/flarum-profile-views/./src/forum/index.js"],"names":["installedModules","__webpack_require__","moduleId","exports","module","i","l","modules","call","m","c","d","name","getter","o","Object","defineProperty","enumerable","get","r","Symbol","toStringTag","value","t","mode","__esModule","ns","create","key","bind","n","object","property","prototype","hasOwnProperty","p","s","flarum","core","compat","app","initializers","add","User","views","Model","attribute","extend","UserCard","items","user","this","props","icon","translator","trans","viewcount"],"mappings":"2BACA,IAAAA,EAAA,GAGA,SAAAC,EAAAC,GAGA,GAAAF,EAAAE,GACA,OAAAF,EAAAE,GAAAC,QAGA,IAAAC,EAAAJ,EAAAE,GAAA,CACAG,EAAAH,EACAI,GAAA,EACAH,QAAA,IAUA,OANAI,EAAAL,GAAAM,KAAAJ,EAAAD,QAAAC,IAAAD,QAAAF,GAGAG,EAAAE,GAAA,EAGAF,EAAAD,QA0DA,OArDAF,EAAAQ,EAAAF,EAGAN,EAAAS,EAAAV,EAGAC,EAAAU,EAAA,SAAAR,EAAAS,EAAAC,GACAZ,EAAAa,EAAAX,EAAAS,IACAG,OAAAC,eAAAb,EAAAS,EAAA,CAA0CK,YAAA,EAAAC,IAAAL,KAK1CZ,EAAAkB,EAAA,SAAAhB,GACA,oBAAAiB,eAAAC,aACAN,OAAAC,eAAAb,EAAAiB,OAAAC,YAAA,CAAwDC,MAAA,WAExDP,OAAAC,eAAAb,EAAA,cAAiDmB,OAAA,KAQjDrB,EAAAsB,EAAA,SAAAD,EAAAE,GAEA,GADA,EAAAA,IAAAF,EAAArB,EAAAqB,IACA,EAAAE,EAAA,OAAAF,EACA,KAAAE,GAAA,iBAAAF,QAAAG,WAAA,OAAAH,EACA,IAAAI,EAAAX,OAAAY,OAAA,MAGA,GAFA1B,EAAAkB,EAAAO,GACAX,OAAAC,eAAAU,EAAA,WAAyCT,YAAA,EAAAK,UACzC,EAAAE,GAAA,iBAAAF,EAAA,QAAAM,KAAAN,EAAArB,EAAAU,EAAAe,EAAAE,EAAA,SAAAA,GAAgH,OAAAN,EAAAM,IAAqBC,KAAA,KAAAD,IACrI,OAAAF,GAIAzB,EAAA6B,EAAA,SAAA1B,GACA,IAAAS,EAAAT,KAAAqB,WACA,WAA2B,OAAArB,EAAA,SAC3B,WAAiC,OAAAA,GAEjC,OADAH,EAAAU,EAAAE,EAAA,IAAAA,GACAA,GAIAZ,EAAAa,EAAA,SAAAiB,EAAAC,GAAsD,OAAAjB,OAAAkB,UAAAC,eAAA1B,KAAAuB,EAAAC,IAGtD/B,EAAAkC,EAAA,GAIAlC,IAAAmC,EAAA,mBClFAhC,EAAAD,QAAAkC,OAAAC,KAAAC,OAAA,mBCAAnC,EAAAD,QAAAkC,OAAAC,KAAAC,OAAA,8BCAAnC,EAAAD,QAAAkC,OAAAC,KAAAC,OAAA,sCCAAnC,EAAAD,QAAAkC,OAAAC,KAAAC,OAAA,+BCAAnC,EAAAD,QAAAkC,OAAAC,KAAAC,OAAA,qBCAAnC,EAAAD,QAAAkC,OAAAC,KAAAC,OAAA,uICOAC,IAAIC,aAAaC,IAAI,sCAAuC,WACxDC,IAAKV,UAAUW,MAAQC,IAAMC,UAAU,SAEvCC,iBAAOC,IAASf,UAAW,YAAa,SAASgB,GAC7C,IAAMC,EAAOC,KAAKC,MAAMF,KAExBD,EAAMP,IAAI,gBACNjC,EAAA,YACK4C,IAAK,cACL,IACAb,IAAIc,WAAWC,MAAM,mDAAoD,CAACC,UAA2B,GAAhBN,EAAKN,QAAe,IAAMM,EAAKN","file":"forum.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 6);\n","module.exports = flarum.core.compat['app'];","module.exports = flarum.core.compat['models/User'];","module.exports = flarum.core.compat['components/UserCard'];","module.exports = flarum.core.compat['helpers/icon'];","module.exports = flarum.core.compat['Model'];","module.exports = flarum.core.compat['extend'];","import app from 'flarum/app';\r\nimport User from 'flarum/models/User';\r\nimport UserCard from 'flarum/components/UserCard';\r\nimport icon from 'flarum/helpers/icon';\r\nimport Model from 'flarum/Model';\r\nimport { extend } from 'flarum/extend';\r\n\r\napp.initializers.add('michaelbelgium-flarum-profile-views', function() {\r\n User.prototype.views = Model.attribute('views');\r\n\r\n extend(UserCard.prototype, 'infoItems', function(items) {\r\n const user = this.props.user;\r\n\r\n items.add('profile-views',(\r\n \r\n {icon('far fa-eye')}\r\n {' '}\r\n {app.translator.trans('flarum_profile_views.forum.user.views_count_text', {viewcount: user.views() == 0 ? '0' : user.views()})}\r\n \r\n ));\r\n });\r\n});"],"sourceRoot":""} \ No newline at end of file +{"version":3,"sources":["webpack://@michaelbelgium/flarum-profile-views/webpack/bootstrap","webpack://@michaelbelgium/flarum-profile-views/external \"flarum.core.compat['Model']\"","webpack://@michaelbelgium/flarum-profile-views/external \"flarum.core.compat['extend']\"","webpack://@michaelbelgium/flarum-profile-views/external \"flarum.core.compat['components/UserPage']\"","webpack://@michaelbelgium/flarum-profile-views/external \"flarum.core.compat['models/User']\"","webpack://@michaelbelgium/flarum-profile-views/external \"flarum.core.compat['components/UserCard']\"","webpack://@michaelbelgium/flarum-profile-views/external \"flarum.core.compat['components/FieldSet']\"","webpack://@michaelbelgium/flarum-profile-views/external \"flarum.core.compat['helpers/icon']\"","webpack://@michaelbelgium/flarum-profile-views/external \"flarum.core.compat['helpers/avatar']\"","webpack://@michaelbelgium/flarum-profile-views/external \"flarum.core.compat['helpers/username']\"","webpack://@michaelbelgium/flarum-profile-views/external \"flarum.core.compat['utils/ItemList']\"","webpack://@michaelbelgium/flarum-profile-views/external \"flarum.core.compat['utils/humanTime']\"","webpack://@michaelbelgium/flarum-profile-views/./node_modules/@babel/runtime/helpers/esm/assertThisInitialized.js","webpack://@michaelbelgium/flarum-profile-views/./node_modules/@babel/runtime/helpers/esm/defineProperty.js","webpack://@michaelbelgium/flarum-profile-views/./src/ProfileView.js","webpack://@michaelbelgium/flarum-profile-views/./node_modules/@babel/runtime/helpers/esm/inheritsLoose.js","webpack://@michaelbelgium/flarum-profile-views/./src/forum/index.js"],"names":["installedModules","__webpack_require__","moduleId","exports","module","i","l","modules","call","m","c","d","name","getter","o","Object","defineProperty","enumerable","get","r","Symbol","toStringTag","value","t","mode","__esModule","ns","create","key","bind","n","object","property","prototype","hasOwnProperty","p","s","flarum","core","compat","_assertThisInitialized","self","ReferenceError","_defineProperty","obj","configurable","writable","ProfileView","subClass","superClass","Model","attribute","transformDate","hasOne","constructor","__proto__","app","initializers","add","store","models","userprofileview","User","profileViews","hasMany","extend","UserCard","items","user","this","props","count","length","icon","translator","trans","viewcount","UserPage","lastViewed","ItemList","views","slice","forEach","pv","viewer","id","href","forum","username","avatar","className","title","visitedAt","toLocaleString","humanTime","FieldSet","component","label","children","toArray","session","request","method","url","data","viewedUser"],"mappings":"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","file":"forum.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 11);\n","module.exports = flarum.core.compat['Model'];","module.exports = flarum.core.compat['extend'];","module.exports = flarum.core.compat['components/UserPage'];","module.exports = flarum.core.compat['models/User'];","module.exports = flarum.core.compat['components/UserCard'];","module.exports = flarum.core.compat['components/FieldSet'];","module.exports = flarum.core.compat['helpers/icon'];","module.exports = flarum.core.compat['helpers/avatar'];","module.exports = flarum.core.compat['helpers/username'];","module.exports = flarum.core.compat['utils/ItemList'];","module.exports = flarum.core.compat['utils/humanTime'];","export default function _assertThisInitialized(self) {\n if (self === void 0) {\n throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\");\n }\n\n return self;\n}","export default function _defineProperty(obj, key, value) {\n if (key in obj) {\n Object.defineProperty(obj, key, {\n value: value,\n enumerable: true,\n configurable: true,\n writable: true\n });\n } else {\n obj[key] = value;\n }\n\n return obj;\n}","import Model from 'flarum/Model';\r\n\r\nexport default class ProfileView extends Model {\r\n //comes from ProfileViewSerializer (viewer(), viewedUser(), visited_at)\r\n visitedAt = Model.attribute('visited_at', Model.transformDate);\r\n viewer = Model.hasOne('viewer');\r\n viewedUser = Model.hasOne('viewedUser');\r\n}","export default function _inheritsLoose(subClass, superClass) {\n subClass.prototype = Object.create(superClass.prototype);\n subClass.prototype.constructor = subClass;\n subClass.__proto__ = superClass;\n}","import User from 'flarum/models/User';\r\nimport UserPage from 'flarum/components/UserPage';\r\nimport UserCard from 'flarum/components/UserCard';\r\nimport FieldSet from 'flarum/components/FieldSet';\r\nimport icon from 'flarum/helpers/icon';\r\nimport avatar from 'flarum/helpers/avatar';\r\nimport username from 'flarum/helpers/username';\r\nimport Model from 'flarum/Model';\r\nimport ItemList from 'flarum/utils/ItemList';\r\nimport { extend } from 'flarum/extend';\r\nimport humanTime from 'flarum/utils/humanTime';\r\nimport ProfileView from '../ProfileView';\r\n\r\napp.initializers.add('michaelbelgium-flarum-profile-views', function() {\r\n app.store.models.userprofileview = ProfileView;//\".userprofileview\" = serializer type \"userprofileview\"\r\n User.prototype.profileViews = Model.hasMany('profileViews');//comes from AddUserProfileViewsRelationship::RELATIONSHIP = php model relationship method\r\n\r\n extend(UserCard.prototype, 'infoItems', function(items) {\r\n const user = this.props.user;\r\n\r\n const count = user.profileViews() === false ? 0 : user.profileViews().length;\r\n\r\n items.add('profile-views',(\r\n \r\n {icon('far fa-eye')}\r\n {' '}\r\n {app.translator.trans('flarum_profile_views.forum.user.views_count_text', {viewcount: '' + count})}\r\n \r\n ));\r\n });\r\n\r\n extend(UserPage.prototype, 'sidebarItems', function(items) {\r\n const lastViewed = new ItemList();\r\n\r\n var views = this.user.profileViews();\r\n\r\n if(views !== false)\r\n {\r\n if(views.length >= 5) {\r\n views = views.slice(0, 5);\r\n }\r\n \r\n views.forEach(pv => {\r\n lastViewed.add('lastUser-' + pv.viewer().id(),\r\n \r\n {avatar(pv.viewer(), {className: 'lastUser-avatar'})}\r\n
\r\n {username(pv.viewer())}\r\n {humanTime(pv.visitedAt())}\r\n
\r\n
\r\n );\r\n });\r\n }\r\n\r\n items.add('lastViewedUsers', FieldSet.component({\r\n label: app.translator.trans('flarum_profile_views.forum.user.title_last_viewers'),\r\n className: 'LastUsers',\r\n children: lastViewed.toArray()\r\n }));\r\n });\r\n\r\n extend(UserPage.prototype, 'show', function() {\r\n if(typeof app.session.user !== 'undefined' && app.session.user.id() !== this.user.id())\r\n {\r\n app.request({\r\n method: 'POST',\r\n url: app.forum.attribute('apiUrl') + '/profileview',\r\n data: { \r\n viewer: app.session.user.id(),\r\n viewedUser: this.user.id()\r\n }\r\n });\r\n }\r\n });\r\n});"],"sourceRoot":""} \ No newline at end of file diff --git a/js/src/ProfileView.js b/js/src/ProfileView.js new file mode 100644 index 0000000..a20b234 --- /dev/null +++ b/js/src/ProfileView.js @@ -0,0 +1,8 @@ +import Model from 'flarum/Model'; + +export default class ProfileView extends Model { + //comes from ProfileViewSerializer (viewer(), viewedUser(), visited_at) + visitedAt = Model.attribute('visited_at', Model.transformDate); + viewer = Model.hasOne('viewer'); + viewedUser = Model.hasOne('viewedUser'); +} \ No newline at end of file diff --git a/js/src/forum/index.js b/js/src/forum/index.js index f422188..5950063 100644 --- a/js/src/forum/index.js +++ b/js/src/forum/index.js @@ -1,22 +1,76 @@ -import app from 'flarum/app'; import User from 'flarum/models/User'; +import UserPage from 'flarum/components/UserPage'; import UserCard from 'flarum/components/UserCard'; +import FieldSet from 'flarum/components/FieldSet'; import icon from 'flarum/helpers/icon'; +import avatar from 'flarum/helpers/avatar'; +import username from 'flarum/helpers/username'; import Model from 'flarum/Model'; +import ItemList from 'flarum/utils/ItemList'; import { extend } from 'flarum/extend'; +import humanTime from 'flarum/utils/humanTime'; +import ProfileView from '../ProfileView'; app.initializers.add('michaelbelgium-flarum-profile-views', function() { - User.prototype.views = Model.attribute('views'); + app.store.models.userprofileview = ProfileView;//".userprofileview" = serializer type "userprofileview" + User.prototype.profileViews = Model.hasMany('profileViews');//comes from AddUserProfileViewsRelationship::RELATIONSHIP = php model relationship method extend(UserCard.prototype, 'infoItems', function(items) { const user = this.props.user; + const count = user.profileViews() === false ? 0 : user.profileViews().length; + items.add('profile-views',( {icon('far fa-eye')} {' '} - {app.translator.trans('flarum_profile_views.forum.user.views_count_text', {viewcount: user.views() == 0 ? '0' : user.views()})} + {app.translator.trans('flarum_profile_views.forum.user.views_count_text', {viewcount: '' + count})} )); }); + + extend(UserPage.prototype, 'sidebarItems', function(items) { + const lastViewed = new ItemList(); + + var views = this.user.profileViews(); + + if(views !== false) + { + if(views.length >= 5) { + views = views.slice(0, 5); + } + + views.forEach(pv => { + lastViewed.add('lastUser-' + pv.viewer().id(), + + {avatar(pv.viewer(), {className: 'lastUser-avatar'})} +
+ {username(pv.viewer())} + {humanTime(pv.visitedAt())} +
+
+ ); + }); + } + + items.add('lastViewedUsers', FieldSet.component({ + label: app.translator.trans('flarum_profile_views.forum.user.title_last_viewers'), + className: 'LastUsers', + children: lastViewed.toArray() + })); + }); + + extend(UserPage.prototype, 'show', function() { + if(typeof app.session.user !== 'undefined' && app.session.user.id() !== this.user.id()) + { + app.request({ + method: 'POST', + url: app.forum.attribute('apiUrl') + '/profileview', + data: { + viewer: app.session.user.id(), + viewedUser: this.user.id() + } + }); + } + }); }); \ No newline at end of file diff --git a/less/extension.less b/less/extension.less new file mode 100644 index 0000000..8f27ef0 --- /dev/null +++ b/less/extension.less @@ -0,0 +1,37 @@ +fieldset.LastUsers { + ul { + list-style-type: none; + padding: 0; + + li a { + display: block; + + span.lastUser-avatar { + width: 20%; + display: inline-block; + height: 35px; + line-height: 35px; + } + + div { + display: inline-block; + width: 80%; + + span.username { + margin-left: 6px; + } + + span.lastUser-visited { + float: right; + font-size: .8em; + } + } + } + + li a:hover { + div > span { + text-decoration: underline; + } + } + } +} \ No newline at end of file diff --git a/locale/en.yml b/locale/en.yml index b137e95..756040c 100644 --- a/locale/en.yml +++ b/locale/en.yml @@ -1,4 +1,5 @@ flarum_profile_views: forum: user: - views_count_text: viewed {viewcount} times \ No newline at end of file + views_count_text: viewed {viewcount} times + title_last_viewers: Last profile viewers \ No newline at end of file diff --git a/migrations/2019_01_12_103741_drop_ip_views.php b/migrations/2019_01_12_103741_drop_ip_views.php new file mode 100644 index 0000000..ea3ebaa --- /dev/null +++ b/migrations/2019_01_12_103741_drop_ip_views.php @@ -0,0 +1,7 @@ + ["string"] +]); \ No newline at end of file diff --git a/migrations/2019_01_12_104853_add_visited_at_views.php b/migrations/2019_01_12_104853_add_visited_at_views.php new file mode 100644 index 0000000..15d056d --- /dev/null +++ b/migrations/2019_01_12_104853_add_visited_at_views.php @@ -0,0 +1,7 @@ + ["datetime"] +]); \ No newline at end of file diff --git a/src/Listeners/AddUserProfileViewsRelationship.php b/src/Listeners/AddUserProfileViewsRelationship.php new file mode 100644 index 0000000..a26f71f --- /dev/null +++ b/src/Listeners/AddUserProfileViewsRelationship.php @@ -0,0 +1,66 @@ +profileViews() + const RELATIONSHIP_OTHER = "viewedProfiles"; //$user->viewedProfiles() + + /** + * @param Dispatcher $events + */ + public function subscribe(Dispatcher $events) + { + $events->listen(WillGetData::class, [$this, 'includeTagsRelationship']); + $events->listen(GetModelRelationship::class, [$this, 'getModelRelationship']); + $events->listen(GetApiRelationship::class, [$this, 'getApiRelationship']); + } + + public function getModelRelationship(GetModelRelationship $event) + { + if($event->isRelationship(User::class, self::RELATIONSHIP)) + { + return $event->model->hasMany(UserProfileView::class, 'viewed_user_id'); + } + + if($event->isRelationship(User::class, self::RELATIONSHIP_OTHER)) + { + return $event->model->hasMany(UserProfileView::class, 'viewer_id'); + } + } + + /** + * @param GetApiRelationship $event + * @return \Tobscure\JsonApi\Relationship|null + */ + public function getApiRelationship(GetApiRelationship $event) + { + if ($event->isRelationship(UserSerializer::class, self::RELATIONSHIP)) { + return $event->serializer->hasMany($event->model, UserProfileViewSerializer::class, self::RELATIONSHIP); + } + + //todo test: + if ($event->isRelationship(UserSerializer::class, self::RELATIONSHIP_OTHER)) { + return $event->serializer->hasMany($event->model, UserProfileViewSerializer::class, self::RELATIONSHIP_OTHER); + } + } + + /** + * @param WillGetData $event + */ + public function includeTagsRelationship(WillGetData $event) + { + if($event->controller->serializer == UserSerializer::class) + $event->addInclude([self::RELATIONSHIP, self::RELATIONSHIP.'.viewer', self::RELATIONSHIP.'.viewedUser']);//".x" comes from model relationship UserProfileView + } +} \ No newline at end of file diff --git a/src/Models/UserProfileView.php b/src/Models/UserProfileView.php new file mode 100644 index 0000000..d21ae42 --- /dev/null +++ b/src/Models/UserProfileView.php @@ -0,0 +1,22 @@ +belongsTo(User::class, 'viewed_user_id'); + } + + public function viewer() + { + return $this->belongsTo(User::class, 'viewer_id'); + } +} diff --git a/src/Serializers/UserProfileViewSerializer.php b/src/Serializers/UserProfileViewSerializer.php new file mode 100644 index 0000000..3061142 --- /dev/null +++ b/src/Serializers/UserProfileViewSerializer.php @@ -0,0 +1,28 @@ + $this->formatDate($profileview->visited_at) + ]; + } + + protected function viewedUser($profileview) + { + return $this->hasOne($profileview, UserSerializer::class); + } + + protected function viewer($profileview) + { + return $this->hasOne($profileview, UserSerializer::class); + } +} \ No newline at end of file diff --git a/src/controllers/CreateUserProfileViewController.php b/src/controllers/CreateUserProfileViewController.php new file mode 100644 index 0000000..c4941e7 --- /dev/null +++ b/src/controllers/CreateUserProfileViewController.php @@ -0,0 +1,34 @@ +getParsedBody(), 'viewedUser'); + $viewerId = array_get($request->getParsedBody(), 'viewer'); + $viewedUser = User::find($viewedUserId); + + $profileView = $viewedUser->profileViews()->where('viewer_id', $viewerId)->first(); + + if(is_null($profileView)) { + $profileView = new UserProfileView(); + $profileView->viewedUser()->associate($viewedUser); + $profileView->viewer()->associate(User::find($viewerId)); + } + + $profileView->visited_at = Carbon::now(); + $profileView->save(); + + return new JsonResponse($profileView->toArray()); + } +} \ No newline at end of file diff --git a/src/listeners/AddProfileViewHandler.php b/src/listeners/AddProfileViewHandler.php deleted file mode 100644 index caf608f..0000000 --- a/src/listeners/AddProfileViewHandler.php +++ /dev/null @@ -1,41 +0,0 @@ -listen(WillSerializeData::class, [$this, "confViews"]); - } - - public function confViews(WillSerializeData $event) - { - if($event->isController(ShowUserController::class)) - { - $id = $event->request->getQueryParams()["id"]; - if(is_numeric($id)) return; - - $serverParams = $event->request->getServerParams(); - $ip = $serverParams['REMOTE_ADDR']; - - if (isset($serverParams["HTTP_CF_CONNECTING_IP"])) - $ip = $serverParams["HTTP_CF_CONNECTING_IP"]; - - $visited_user = $event->data; - $user = $event->actor; - - $resultCount = $visited_user->profileViews()->wherePivot('ip', '=', $ip)->count(); - - if($resultCount > 0 || $user->isGuest()) return; - - $visited_user->profileViews()->attach($user, ["ip" => $ip]); - } - } -} diff --git a/src/listeners/AddUserProfileViewsRelationship.php b/src/listeners/AddUserProfileViewsRelationship.php deleted file mode 100644 index c211386..0000000 --- a/src/listeners/AddUserProfileViewsRelationship.php +++ /dev/null @@ -1,33 +0,0 @@ -listen(GetModelRelationship::class, [$this, 'getModelRelationship']); - } - - public function getModelRelationship(GetModelRelationship $event) - { - if($event->isRelationship(User::class, self::RELATIONSHIP)) - { - return $event->model->belongsToMany(User::class, 'user_profile_views', 'viewed_user_id', 'viewer_id')->withPivot('ip'); - } - - if($event->isRelationship(User::class, self::RELATIONSHIP_OTHER)) - { - return $event->model->belongsToMany(User::class, 'user_profile_views', 'viewer_id', 'viewed_user_id')->withPivot('ip'); - } - } -} \ No newline at end of file