Jump to content
View in the app

A better way to browse. Learn more.

Invision Community

A full-screen app on your home screen with push notifications, badges and more.

To install this app on iOS and iPadOS
  1. Tap the Share icon in Safari
  2. Scroll the menu and tap Add to Home Screen.
  3. Tap Add in the top-right corner.
To install this app on Android
  1. Tap the 3-dot menu (⋮) in the top-right corner of the browser.
  2. Tap Add to Home screen or Install app.
  3. Confirm by tapping Install.

Mark

Clients
  • Joined

  • Last visited

Everything posted by Mark

  1. Introduction Joining my previous entries about content and uploading features in post editor in IPS Social Suite 4.0, I'd like to take you through the customisation features on the editor. Toolbar layout The buttons that appear on the toolbar are completely customisable in 4.0 and you can set different layouts for desktop, tablet and mobile (so that you don't show more buttons than the device can show). This is what the management screen looks like: (This is an unfinished design - the tabs won't be be like that in the final version.) To move a button you just drag and drop. The buttons on the right allow you to add more rows or separators. Clicking on a button brings up a dialog where you can adjust where and to whom it shows: Adding Buttons There are two ways to add a button to the editor. The easiest way is to install a CKEditor plugin. CKEditor has loads of plugins, and installing is as easy as uploading the zip file from their site. Here's a screenshot of the symbol plugin being used: The second way is similar to how custom BBCode currently works, you specify the HTML code to be added when the user clicks on the button. Manually created buttons can optionally have a dialog popup to ask for an option. Design Just as you can install CKEditor plugins by uploading the zip file, you can do exactly the same with CKEditor skins to change the design of the editor. You then simply set for each skin on your community which CKEditor skin to use for it. BBCode Though no features in IPS4 insert BBCode-style tags into the editor (like is currently done for attachments, etc.) users can still type BBCode into the editor and it will work fine. We've rewritten how BBCode is parsed to be much more secure and reliable and produce more standards-compliant HTML (for those who are interested, it parses the post content into a DOM Document and examines only the text nodes for BBCode tags, then either splits the nodes surrounding it and inserts one for block-level elements, or wraps all subsequent text nodes in the appropriate formatting element until the end BBCode is found). The benefit to this is that there now no longer needs to be a "BBCode mode" - you can type BBCode straight into the editor, even complicated stuff like lists spanning multiple lines, and it comes out looking great. The downside to this approach is that custom BBCodes can no longer be added through the Admin CP. However, as mentioned above, we now have the ability to add custom buttons to the editor which work in a much more intuitive way, and can do everything that custom BBCodes could and more. For those who really want to be able to add the ability for custom BBCode, we've isolated the method that returns the supported BBCode (and information needed to parse them) into a specific method so that custom BBCode can be added with a very simple hook specific to that purpose. Conclusion There's still one more blog entry to go in our series on the editor. To finish up I'll be showing off some cool special features including how you can post using regular HTML.
  2. Introduction In the last blog entry I introduced some of the features in the post editor in IPS Social Suite 4.0. In this blog entry I'd like to show you the uploading features in the editor. Using the "Image" and "Attachment" dialogs Along the bottom of the editor there are two buttons that deal with uploading files: image and attachments. Both present a dialog which looks like this: We decided to keep both an images and an attachments dialog as users wanting to insert an image will naturally look for the "Image" button - if however, you upload an image to the attachments dialog, it will work completely as expected. The upload panel here is based on HTML5 which supports drag and drop uploading, if your browser doesn't support this, it will use Flash, Silverlight or Google Gears if you have any of those installed, and if not it will fallback to a HTML4 & JavaScript implementation (none of these support drag and drop, but instead you click the "Choose Files" button just as you do now - the label in the box will change to reflect this). Uploaded files then show below the box (images will get a preview), and you can click on any to add them into the editor, or click the "Insert All" button. When you insert an attachment into the editor, it displays either the image if it's an image, or a link if it's anything else, just as it will actually appear in the post (rather than the current "[attach=XXX]" tag). You can also of course delete the attachment, which will automatically remove it from the editor if you've already inserted it. Video Demonstration Quick drag-and-drop In addition to interacting with the panels, if you're using a supported browser, you can drag and drop straight into the editor. It will automatically figure out whether the uploaded file(s) are images or other files and add them to the appropriate panel automatically. Video Demonstration Image URLs In the image panel, there is an additional "From URL" tab which allows you to insert an image from a URL, as you type the URL a preview is shown, and you can optionally link to the image. Video Demonstration My Files In IP.Board currently, there is a "My Media" button which allows you to insert content submitted either in other posts or elsewhere in the community (images in IP.Gallery or files in IP.Downloads for example) into the editor. In 4.0, this feature is found in the images and attachments dialogs. Just with normal attachments, the content is inserted as it will be shown rather than the current "[sharedmedia=XXX]" tag. Conclusion Please let us know what you think of the uploading features in the comments. Remember though that we're only half way through our series on the 4.0 editor. In my next blog entry I'll be talking about customising the editor and the place of BBCode.
  3. Introduction The post editor is undoubtably one of the most frequently used features of the IPS Social Suite as it's the way users submit content to your community and functionality has evolved dramatically from the early days of forum software which consisted of a plain textbox in which users would type BBCode into the feature-rich WYSIWG (What You See Is What You Get) editors prevalent on the web today. For the IPS Social Suite 4.0 we really wanted to focus on making the editor as good as it can be: feature-rich, intuitive to users and highly customisable. Over the next 4 blog entries I'm going to cover the functionality of the editor and related features (attachments, emoticons, etc.) and the customisation options that will be available in 4.0. The theme of each blog entry will be: Here's a screenshot of what it looks like (by default, on a desktop, more on that later): Quotes A feature added to IP.Board in the 3.x series was "visual quotes" - when adding a quote the box shows directly in the editor as it will show in the post, rather than as just normal text with a quote BBCode wrapped around. In 4.0 we've rewritten how this works to use a CSS3 based solution rather than JavaScript to keep the citation header attached to the quote, which makes the feature much more reliable and easy to use when splitting quote boxes, moving the cursor before or after the quote box or dealing with embedded quotes. Code We've also rewritten how code can be inserted into the editor to be more reliable. Adding code now initially brings up an editor which supports syntax highlighting: And after inserting, you'll see the code exactly how it will be displayed after posting and you can interact with it exactly how you'd expect: Spoilers Spoilers are now also visual (though of course they're not true WYSIWYG as you need to be able to see what you're typing) and a built-in feature enabled by default. When you click the spoiler button you'll be given a darkened box in which to type the content: The spoilers display in the post as the same coloured box with a "click to view" message. When clicked, the dark box fades away revealing the content underneath. Emoticons We've completely overhauled emoticon management in 4.0. You can now create multiple "groups", drag and drop to reorder and quickly upload loads of emoticons at once. When you click the emoticon button in the editor, you'll see a popup with an overview of all the groups. If your community only uses the default emoticons, this is big enough to show all the emoticons, however, if you like to install lots of emoticons, you can select any group from the dropdown menu to show all emoticons in that group, or use the search bar to find a particular emoticon. In addition, your most recently used emoticons will show right at the top. Embedded Media In 3.0 we introduced a media BBCode tag. By wrapping a URL to media such as YouTube or Vimeo videos in media BBCode tags, the correct embed code would automatically be worked out and embedded into the post. In later versions, we added automatic embedding support, so just pasting the URL into the post would cause the video to be embedded. This still works exactly the same in 4.0 with a couple improvements: Previews If you type the URL to a supported media in the "Link" dialog, a preview will be shown in the dialog - you can choose to insert the embedded media or just a regular link. If you insert the embedded media, it will show directly in the editor. oEmbed Currently URLs which gets picked up for media embedding and the output they produce are managed in the Admin CP. The idea was that this would allow admins to add support for additional services, however, it has caused some problems when for example, YouTube has added new URL formats or changed their embed code and they don't work on most communities until we release an update to change the default record for YouTube. is an API which allows you to make a call to the provider giving them the URL and they respond with the best embed code to use, if that URL can be embedded. In 4.0, if a URL for YouTube, Flickr, Vimeo, College Humor or Hulu is inserted, we make a call to their oEmbed provider to obtain the embed code, so it's always up to date. Conclusion This is just the first in our series of blog entries on the editor in IPS4. Please do let us know what you think in the comments, but remember there's much more to come! [*]Content [*]Uploads [*]Customisation and BBCode [*]Special features Video Demonstration Video Demonstration Video Demonstration oEmbed
  4. Mark posted a blog entry in Invision Community
    Introduction I previously wrote a blog entry about building tables in IPS Social Suite 4.0. Similar to tables, we also have trees. Trees in many ways look and behave similarly to tables, but can be distinguished mainly by the fact that trees show a collection of objects in a fixed order (often, though not always the order can be changed by the administrator) whereas tables can show data sorted however you like at the time. An example of trees would be the list of forums in IP.Board. Trees vary quite significantly in their individual implementations, for example: Objects in a tree often have parent/child relationships. Sometimes this relationship is between the same type of object (for example, forums in IP.Board or categories in IP.Downloads), sometimes the relationship is between different types of object (for example, applications and modules, where modules are always the child of applications) and sometimes it's a mix of both (for example, packages in IP.Nexus are always children of package groups, but package groups may or may not be children of each other). Objects in a tree can usually, but not always be reordered by the administrator. Objects in a tree can sometimes be enabled and disabled without being deleted, for example applications and modules. Sometimes individual objects can't when the rest can (for example, you can't disable the "Core" application). Trees usually have controls for adding, editing, assigning permissions, duplicating and deleting objects, though a subset of these controls, or additional controls may be available (for example, you can't edit the permissions on a package in IP.Nexus, but you can run a purchase report, which you can't do in any other trees). For IPS Social Suite 4.0, I wanted to create a central class to create trees - currently we duplicate a lot of functionality, and all our trees display differently (in some places, quite radically so), but without loosing any of the flexibility necessary given the various differences in each implementation. Because trees are both more complicated and flexible than tables, the method for creating one might seem complicated - however, when you compare it to having to write every part manually (including all the JavaScript) as was the case in IP.Board 3, you'll be shaving hours off of development time. There are two ways you can create trees in IPS Social Suite 4.0. The most common way is when each item in the table has a record in the database. There are occasions when this isn't the case (for example, the developer center displays trees created from JSON objects), and the Tree class can handle these, but in this blog entry I'm going to show you the more common method. Just like I did for tables, I'm going to take you through how I programmed a real-world example, specifically the tree for custom profile fields. Creating the classes Custom profile fields are arranged into groups, so I'm going to start by creating a tree that shows the groups. To do this, I need to create a class for the custom profile field groups, I'll set this class to extend IPSNodeModel which is an abstract class that provides most of the functionality we need. IPSNodeModel in turn extends a class called IPSPatternsActiveRecord which provides functionality to make a class an Active Record (by that I mean, an object of the class corresponds to a record in the database). In this class I need to define a few variables - I'll explain them below, but this is the code I'll write: /** * Custom Profile Field Group Node */ class _Group extends IPSNodeModel { /** * @brief [ActiveRecord] Multiton Store */ protected static $multitons; /** * @brief [ActiveRecord] Default Values */ protected static $defaultValues = NULL; /** * @brief [ActiveRecord] Database Table */ public static $databaseTable = 'core_pfields_groups'; /** * @brief [ActiveRecord] Database Prefix */ public static $databasePrefix = 'pf_group_'; /** * @brief [ActiveRecord] ID Database Column */ public static $databaseColumnId = 'id'; /** * @brief [Node] Node Title */ public static $nodeTitle = 'profile_fields'; } $multitons and $defaultValues are required by the IPSPatternsActiveRecord class. We don't need to do anything with them other than declare them. $databaseTable tells the IPSPatternsActiveRecord class what database table the records we need are in. $databasePrefix tells the IPSPatternsActiveRecord class the prefix used on all database columns. This isn't necessary, but since all the columns in my table start with "pf_group_", putting it here will save me typing it out every time. $databaseColumnId tells the IPSPatternsActiveRecord class which column contains the primary key. $nodeTitle tells the IPSNodeModel class what language string to use as the title on my page that shows the tree. Now I need to create a controller to display my tree - the developer center will set up the structure of this for me, then I just fill in the name of the class I just created: namespace IPScoremodulesadminmembersettings; /** * Profile Fields and Settings */ class _profiles extends IPSNodeController { /** * Node Class */ protected $nodeClass = 'IPScoreProfileFieldsGroup'; } Now I have a page which looks like this: Customising the rows You'll notice I now have two rows (because I have two rows in my database), but both are blank. This is because the IPSNodeModel class doesn't know what to use for the record title. Let's fix that by adding a method to our class: /** * [Node] Get Node Title * * @return string */ protected function get__title() { $key = "core_pfieldgroups_{$this->id}"; return IPSLang::i()->$key; } That code might seem a bit confusing, but note: $this->id gets the value of the "pf_group_id" column in the database. This is because the IPSPatternsActiveRecord class provides us with a __get method for retrieving the database row values. This is handy if we want to be able to modify the value returned for whatever reason, as we can override that method. We're retrieving the value for a language key rather than some kind of title field in the database. This is because in IPS Social Suite 4, data like this will be translatable, so if you have more than one language you can display different values depending on the user's language choice. So now we have this (I've clicked the dropdown arrow so you can see it's contents): Most of this is okay, but permissions aren't relevant for custom profile field groups, so let's get rid of that. The IPSNodeModel class has methods for checking if the user has permission to each button it displays (which by default check ACP restrictions, which I'll explain more about later) - we can simply override the method which checks for that button so it always returns false: /** * [Node] Does the currently logged in user have permission to edit permissions for this node? * * @return bool */ public function canManagePermissions() { return false; } And now it's gone: Making the buttons work The first buttons I need to make work are the add on the "root" row, and the edit on each row below that. I'm going to ignore the add button on each record row for now as that is for adding a child record and we haven't got to that yet - it will start working automatically when we add support for children. These buttons will display a form. In a previous blog entry I talked about our form helper class. I'm going to use this to build the add/edit form. To do this, I'll add two methods to my class to display the form and to save it's values - here they are: /** * [Node] Add/Edit Form * * @param IPSHelpersForm $form The form * @return void */ public function form( &$form ) { $form->add( new IPSHelpersFormTranslatable( 'pfield_group_title', NULL, TRUE, array( 'app' => 'core', 'key' => ( $this->id ? "core_pfieldgroups_{$this->id}" : NULL ) ) ) ); } /** * [Node] Save Add/Edit Form * * @param array $values Values from the form * @return void */ public function saveForm( $values ) { if ( !$this->id ) { $this->save(); } IPSLang::saveCustom( 'core', "core_pfieldgroups_{$this->id}", $values['pfield_group_title'] ); } Most of that should be self explanatory - however in the blog entry about forms I didn't mention the Translatable class. If you have one language installed, this will just display a normal text field, however, if you have more than one, it will show one for each language. It then returns an array, which we can pass to IPSLang::saveCustom() to save the values. This is what our form might look like if I have several languages installed: Or, more commonly, if I just have one: Now the add and edit forms are working, but since these are very small forms (they only have one input field) I'd like them to display in a modal popup rather than take the user to a new page (if the user has JavaScript disabled, a new page will do). To do this, I just add a property to my class: /** * @brief [Node] Show forms modally? */ public static $modalForms = TRUE; Next we have the copy and delete buttons. These will actually work by themselves (the central class will handle copying and deleting the records from the database), however, since we have translatable values, we need to make sure those too are copied and deleted appropriately. To do this, I'll override the two methods which handle copying and deleting: /** * [ActiveRecord] Duplicate * * @return void */ public function __clone() { $oldId = $this->id; parent::__clone(); IPSLang::saveCustom( 'core', "core_pfieldgroups_{$this->id}", IPSDb::i()->buildAndFetchAll( array( 'select' => '*', 'from' => 'core_sys_lang_words', 'where' => array( 'word_key=?', "core_pfieldgroups_{$oldId}" ) ), 'lang_id', 'word_custom' ) ); } /** * [ActiveRecord] Delete Record * * @return void */ public function delete() { parent::delete(); IPSLang::deleteCustom( 'core', 'core_pfieldgroups_' . $this->id ); } Search You'll notice that the system has automatically added a search box at the top of the table. In order to make this work, we need to add a simple search method: /** * Search * * @param string $column Column to search * @param string $query Search query * @param string|null $order Column to order by * @return array */ public static function search( $column, $query, $order ) { if ( $column === '_title' ) { $return = array(); foreach ( IPSLang::i()->searchCustom( 'core_pfieldgroups_', $query ) as $key => $value ) { try { $return[ $key ] = self::load( $key ); } catch ( Exception $e ) { } } return $return; } return parent::search( $column, $query, $order ); } Making the rows re-orderable The last thing I need to do to finish the handling of groups is make it so we can drag and drop to reorder them. To do this I just add another property to my class telling IPSPatternsActiveRecord which column contains the order ID: /** * @brief [Node] Order Database Column */ public static $databaseColumnOrder = 'order'; ACP Restrictions It's important of course to make sure we honour ACP restrictions. The easiest way to do this is to create individual ACP restrictions for add/edit/delete (this can be done in the developer center) with a common prefix, and then specify like so: /** * @brief [Node] ACP Restrictions */ protected static $restrictions = array( 'app' => 'core', 'module' => 'membersettings', 'prefix' => 'profilefieldgroups_', ); The system will now look for ACP restrictions with the keys "profilefieldgroups_add", "profilefieldgroups_edit" and "profilefieldgroups_delete" when performing those actions. Children Let's recap what we have so far with a video: http://screencast.com/t/TUBuuYBSBON Now that we have groups sorted, we're going to create another class for the actual fields which will show as children. The process is almost exactly the same as for groups. Since the process is the same, I won't go through the process step-by-step, but here is the class I've written if you're interested: Field.php The only changes are: I've declared two additional properties specifying the class name of the parent ("IPScoreProfileFieldsGroup") and which column contains the parent ID. I've declared an additional method to fetch an icon for the row so we can see what type of field this is. Just like we overwrote canManagePermissions for groups, I've also overridden canAdd in the same way, as you cannot add children to profile fields. Now all we need to do is link them up. To do this, I add a property to my group class telling IPSNodeModel the name of the class which contains children: /** * @brief [Node] Subnode class */ public static $subnodeClass = 'IPScoreProfileFieldsField'; The system will now automatically change the behaviour of our page in the following ways: When clicking on a group, it will expand out to show the fields under it. The search box will include fields as well as groups in its results. When clicking the "Add" button for a group, it will show the field to add a field to that group. When clicking the "Copy" button for a group, you'll have the option to copy children too or not. When clicking the "Delete" button for a group, you'll have the option to move children elsewhere or delete them too. (This is my favourite feature) In addition to being able to drag and drop fields to reorder, you can drag a field out of one group and into another. Here's a video: http://screencast.com/t/5fQwgle3EX
  5. A few weeks ago, I posted a blog entry mentioning a new feature in 4.0 which aims to make development of applications within the IPS Social Suite (both for us and third party modification authors) easier. We focussed on managing the database schema in that blog entry and I'd now like to take you through the other features. Modules Two tabs (one for admin modules and one for front modules) allow you to view all modules and sections in your application. You can add modules (which will both insert it into the database and create the relevant files in the filesystem), change the default section for a module (which previously required a defaultSection.php file) and create new sections. When you're creating a new section, the form looks like this (this is for creating a section for an admin module): The "Type" field controls the code that will be placed in the file created for the section - the options are: "Blank" - which will create the class with no other logic, so the section will be blank. "Table" which will create a class with a boilerplate for displaying a table. "Node Controller" - which will create a boilerplate for displaying a tree of containers such as IP.Board forums, IP.Downloads categories, IP.Nexus groups, etc. We've not posted how this class works, but a future blog entry will give more details. No matter which type you select, the system will automatically generate a file, with a basic class structure already filled in, including ACP restrictions checks, etc. The "Menu Tab" field is admin specific and controls under which tab in the Admin CP the section should show (for example, we have some stuff from the "core" app under the "Look & Feel" tab). Previously, one would have to edit the menu.xml file for the module to add sections, and making sections appear under tabs other the default application tab was very difficult - the new system does it for you. The "ACP Restriction" field is also admin specific and allows you to select an existing ACP restriction which will control who can see the section. It also has a special "Create Restriction" option, which will cause the system to create a restriction, and use that. Previously, one would have to edit the permissions.xml file to create restrictions and then assign them in the menu.xml file. Of course - you can completely bypass this feature and manually create your module folders and section files, but the addition of this feature makes the process much quicker. Admin CP Menu This tab contains a graphical representation of the data which was previously stored in menu.xml files. Admin CP Restrictions This tab contains a graphical representation of the data which was previously stored in permissions.xml files. Extensions Extensions are ways in which applications interact with one another. Previously, you would drop extension files in your applications "/extensions" folder, though there wasn't much reasoning to the structure of the directory, it was difficult to know what extensions were available, and sometimes understanding an extensions requirements was difficult. In 4.0, the extensions directory is more structured - the format is owner app > extension type > extension file (so admin/group_form.php for example, is now core/GroupForm/*.php) so this tab provides a GUI for managing your applications extensions. Applications can also specify a boilerplate file for an extension, so you can see what extensions are available, and clicking the "add" button will create a file with a basic structure to get you going. Settings In 4.0, developers have much more control over how settings are presented, rather than all being dumped in the central "System Settings" table. With this, much of the data that was previously needed in settings.xml is no longer required, so we've simplified the process of creating setting to just providing a key and a default value. Versions The versions tab shows all of the application's versions and the database queries that the upgrader will run when upgrading to that version. It's sort of a combination of the versions.xml file and the setup folder. Queries are automatically added as you modify the database schema. Naturally you can also manually add queries, or specify a custom PHP script to run in that upgrade step.
  6. Login Handlers are the different methods for logging into the IPS Social Suite. We currently support: "Internal", which is for accounts created natively through the suite. Facebook Twitter Microsoft (this is currently referred to as "Windows Live", though they rebranded to "Microsoft Account" a short while ago) LDAP "IPS Connect", which is our SSO solution for connecting your site with other IPS Social Suite installations or third-party applications. A generic handler for any MySQL database you have access to. In 4.0 we've made a number of changes to the Login Handlers which I wanted to mention. Improved Password Encryption We currently use a salted md5 hash for hashing passwords. md5 has been a popular password hashing technique for years - however, it is not the most secure hashing method. md5 is designed to be computationally efficient (meaning generating a hash is quick). The problem with this is that if a server were ever compromised to the point that someone were able to gain access to a database containing passwords hashed using md5, and someone were to use a program to generate and hash different strings repeatedly until a match were found, the password could be worked out. One particularly well-known program claims to be able to make 5.6 billion md5 hashes per second with a relatively modern GPU. Even with our hashing method which includes multi-level hashing and a salt, this means, assuming an 8-character long password using only alphanumeric characters were used, a password could be calculated in about 3 days. While I'm unaware of any cases of this actually happening, we want to make sure that our products are as secure as they can be. For this reason, in 4.0, we're migrating to Blowfish. Blowfish is a more cryptographically secure technique for generating hashes that is deliberately slow, meaning that even if your database were ever compromised, the passwords will still be secure. New Login Handlers In addition to the Login Handlers mentioned above, we've added support for Google and LinkedIn. Improved Facebook and Twitter support Currently, although you can log in with Facebook and Twitter, they're not treated on the back-end as true Login Handers. This is because of how Login Handlers in 3.x were designed (which was before such 3rd party login services were popular) in that they assumed you would provide a username (or email) and password directly into a form, and subsequently didn't accommodate the OAuth-style login processes. Since we've rewritten the way Login Handlers are designed, this means we can treat Facebook and Twitter (and Google and LinkedIn which both also use OAuth) exactly the same as the rest. Practically, this means you'll see Facebook and Twitter in the Login Handlers section of the Admin CP, and manage them as you would any other login method. Updated Microsoft Support Microsoft now support OAuth for login through them so we've updated to use that. In addition to being necessary for when they stop supporting the old way, it's much easier to set up for the administrator.
  7. Mark posted a blog entry in Invision Community
    There's a table in the Admin CP of the IPS Social Suite that I really like - the members table. It has some really cool options - you can reorder the data just by clicking on a column head; you can quickly search for a member by typing a name into a search box at the top; there's some filter options to quickly show banned, locked, spam and validating members; and there's an advanced search form to search for members based on practically any criteria. It would be great if these features were available elsewhere. So much like we did for forms, we decided to create a central helper class for building tables. To demonstrate how it works, I'm going to go through, step by step, how I recreated the Admin CP members table in IPS 4. It starts with one line to create the table, and another to pass it to the output class: /* Create the table */ $table = new IPSHelpersTableDb( 'core_members', 'app=core&module=members&section=members' ); /* Display */ IPSOutput::i()->output = IPSOutput::i()->getTemplate( 'global' )->block( 'members', $table ); With just those two lines, you'll see this: Some things to note: We're calling IPSHelpersTableDb - the "Db" part indicates that the source of data for our table is a database table. There are other classes to use, for example, a JSON document as the data source. We pass it the name of our database table (or for the other classes, whatever the data source is) and the query string part of the URL where we're going to be displaying this (which we need to build the links and AJAX calls). I'm passing it to the output through a template called "block" which simply adds the dark-blue bar at the top, which isn't actually part of the table itself, and some padding. The "members" parameter is the key for the langauge string to use in that dark-blue bar. I'm passing $table directly to the template - the helper class has a __toString method which renders the table, so the output class thinks it's been given a normal string. The first obvious thing is that we're showing all the columns in the database table, which obviously we don't want. So let's add another line to specify which columns we want: $table->include = array( 'name', 'email', 'joined', 'member_group_id', 'ip_address' ); In this example, I'm giving the helper class a list of columns to include - I could alternatively pass a list of columns to exclude, if that would be more appropriate. The output is now this: Some things to note: It's worked out pagination itself. When you click a pagination link, the contents of the table will update with AJAX, including changing your browser's URL (unless you have JavaScript disabled of course, in which case it will work like a normal link). Pagination defaults to 25 results per page, but you can change that just by changing a property in the class. All the columns are clickable, which will resort the results. You can sort any column ascending or descending. Resorting will also update with AJAX (including changing your browser's URL), unless JavaScript is disabled. I want the headers to display something more meaningful than the column name. The system will automatically look for language strings which match the column name - you can also optionally specify a prefix, and it'll look for langauge strings which match that followed by the column name. Let's specify a prefix: $table->langPrefix = 'members_'; And I'll then create some language strings that match that (so "members_name", "members_email", etc.). The output is now this: Next - we need to change how we display some of those values. The joined date and the group are displaying the raw values from the database, but we want something more meaningful than that. To format the values, we simply create an array of lambda functions - one for each we want to format: $table->parsers = array( 'joined' => function( $val, $row ) { return IPSDateTime::ts( $val )->localeDate(); }, 'member_group_id' => function( $val, $row ) { return IPSMemberGroup::load( $val )->formattedName(); } ); I'm also going to add one additional line to specify the "main" column, which applies some additional styles: $table->mainColumn = 'name'; The output is now this: Some things to note: I'm using the IPSDateTime class to format the joined date. The ts method in this is a factory method which takes a UNIX timestamp and returns an object of IPSDateTime. IPSDateTime extends DateTime, so all the features of that class are available to us. The localeDate method returns a string with the date formatted appropriately according to user's locale. The IPSMemberGroup::load call being executed for each result may look like it might be resource intensive, but it caches objects it creates, so it's only actually "loading" each group once. Now I want to add a column with the user's photo. There isn't a single "photo" column in the database we can use for this (since the photo could be one they uploaded, a photo from their Facebook account if they're using Facebook Connect, a Gravatar image, or some other things), we need to use a method in the IPSMember class. This isn't a problem. I can simply add an element to our list of fields to include and add that into the parsers. $table->include = array( 'photo', 'name', 'email', 'joined', 'member_group_id', 'ip_address' ); $table->parsers = array( 'photo' => function( $val, $row ) { return IPSMember::constructFromData( $row )->photo('mini'); }, I'll also want to specify that we cannot use the photo column for sorting: $table->noSort = array( 'photo' ); The output is now this: Some things to note: Since this isn't a value which exists in the database, the value of $val in the lambda function will be NULL, however, $row has all the data for that record. We're not using IPSMember::load to get the member object, since that would execute an additional query for every result, which would be resource intensive, and unnecessary since we already have that data. Instead, we use the constructFromData method and pass it the row from the database. Next, I want to specify the default sorting. This is done with just two lines of code: $table->sortBy = $table->sortBy ?: 'joined'; $table->sortDirection = $table->sortDirection ?: 'desc'; The output is now this: Now, I want to add a quick search box. All we need to do is specify which column the quick search should look at: $table->quickSearch = 'name'; The output is now this: Some things to note: As you type, results are obtained with AJAX. You can page through your results (the number of pages will update automatically) and reorder your results by clicking the headers without loosing your search. I also want to allow more advanced search options - like to search by email address, or joined date. To do this, I create a new array: $table->advancedSearch = array( 'member_id' => IPSHelpersTableSEARCH_CONTAINS_TEXT, 'email' => IPSHelpersTableSEARCH_CONTAINS_TEXT, 'ip_address' => IPSHelpersTableSEARCH_CONTAINS_TEXT, 'member_group_id' => array( IPSHelpersTableSEARCH_SELECT, array( 'options' => $groups ), function( $val ) { return array( 'member_group_id=? OR ? IN(mgroup_others)', $val, $val ); } ), 'joined' => IPSHelpersTableSEARCH_DATE_RANGE, ); To explain what's going on here: The keys are the columns we're letting the user search on. The values are usually a constant indicating the type of search that is appropriate for that column. The member_group_id element is a bit more complicated. It has to specify an array of options (I've omitted the code to generate $groups in this snippet, but it'll be at the end of this blog entry), and, because we need to search both primary and secondary groups based on the value, there's a lambda function to get the proper WHERE clause for the query. Now, next to the quick search box there's a button which will bring up a modal popup (or just take you to a new page if JavaScript is disabled) which looks like this: Some things to note:The date entry boxes use the HTML5 date input type: If your browser doesn't support that, there's a JavaScript fallback: And if you're really awkward and are using a browser that doesn't support the HTML5 date input type and have JavaScript disabled, you'll see a regular text box where you can enter a date in practically any format, and it'll work it out. After performing the search, you can reorder your results by clicking the headers without loosing your search. Now, I want to add some filters so you can quickly see banned, spam, locked and validating members. To do this, you create an array simply specifying the WHERE clause to use in the query for each filter: /* Filters */ $table->filters = array( 'members_filter_banned' => 'member_banned=1', 'members_filter_locked' => 'failed_login_count>=' . (int) IPSSettings::i()->ipb_bruteforce_attempts, 'members_filter_spam' => '(members_bitoptions & ' . IPSMember::$bitOptions['bw_is_spammer'] . ') != 0', 'members_filter_validating' => 'v.lost_pass=0 AND v.vid IS NOT NULL' ); For this though, I'll also need to join the core_validating database table, so we add one more line for that: $table->joins = array( array( 'from' => array( 'core_validating' => 'v' ), 'where' => 'v.member_id=_0.member_id' ) ); The output is now this: Some things to note: The helper class will add the "All" filter automatically. It's getting the word to use for the filter by looking for a language string with the same key as the key in the array passed. Like everything else, clicking a filter updates the results with AJAX and the filter is retained in searches. Finally, the last thing I need to do is add a column with some buttons. You can specify a normal array for buttons to show in the header, and a lambda functions to return an array for buttons to show for each row: $table->rootButtons = array( 'add' => array( 'icon' => array( 'icons/add.png', 'core' ), 'title' => 'members_add', 'link' => 'app=members&module=members&section=members&do=add', ) ); $table->rowButtons = function( $row ) { return array( 'edit' => array( 'icon' => array( 'icons/edit.png', 'core' ), 'title' => 'edit', 'link' => 'app=members&module=members&section=members&do=edit&id=' . $row['member_id'], ), 'delete' => array( 'icon' => array( 'icons/delete.png', 'core' ), 'title' => 'delete', 'link' => 'app=members&module=members&section=members&do=delete&id=' . $row['member_id'], 'class' => 'delete', ), ); }; Our finished table looks like this: And behaves like this: http://screencast.com/t/KMFq8zCE To recap, here's the code, in it's entirety to generate that table: /* Create the table */ $table = new IPSHelpersTableDb( 'core_members', 'app=core&module=members&section=members' ); $table->langPrefix = 'members_'; /* Columns we need */ $table->include = array( 'photo', 'name', 'email', 'joined', 'member_group_id', 'ip_address' ); $table->mainColumn = 'name'; $table->noSort = array( 'photo' ); /* Default sort options */ $table->sortBy = $table->sortBy ?: 'joined'; $table->sortDirection = $table->sortDirection ?: 'desc'; /* Filters */ $table->joins = array( array( 'from' => array( 'core_validating' => 'v' ), 'where' => 'v.member_id=_0.member_id' ) ); $table->filters = array( 'members_filter_banned' => 'member_banned=1', 'members_filter_locked' => 'failed_login_count>=' . (int) IPSSettings::i()->ipb_bruteforce_attempts, /*@todo*/ 'members_filter_spam' => '(members_bitoptions & ' . IPSMember::$bitOptions['bw_is_spammer'] . ') != 0', 'members_filter_validating' => 'v.lost_pass=0 AND v.vid IS NOT NULL' ); /* Groups for advanced filter (need to do it this way because array_merge renumbers the result */ $groups = array( '' => 'any_group' ); foreach ( IPSMemberGroup::groups() as $k => $v ) { $groups[ $k ] = $v; } /* Search */ $table->quickSearch = 'name'; $table->advancedSearch = array( 'member_id' => IPSHelpersTableSEARCH_CONTAINS_TEXT, 'email' => IPSHelpersTableSEARCH_CONTAINS_TEXT, 'ip_address' => IPSHelpersTableSEARCH_CONTAINS_TEXT, 'member_group_id' => array( IPSHelpersTableSEARCH_SELECT, array( 'options' => $groups ), function( $val ) { return array( 'member_group_id=? OR ? IN(mgroup_others)', $val, $val ); } ), 'joined' => IPSHelpersTableSEARCH_DATE_RANGE, ); /* Custom parsers */ $table->parsers = array( 'photo' => function( $val, $row ) { return IPSMember::constructFromData( $row )->photo('mini'); }, 'joined' => function( $val, $row ) { return IPSDateTime::ts( $val )->localeDate(); }, 'member_group_id' => function( $val, $row ) { return IPSMemberGroup::load( $val )->formattedName(); } ); /* Specify the buttons */ $table->rootButtons = array( 'add' => array( 'icon' => array( 'icons/add.png', 'core' ), 'title' => 'members_add', 'link' => 'app=members&module=members&section=members&do=add', ) ); $table->rowButtons = function( $row ) { return array( 'edit' => array( 'icon' => array( 'icons/edit.png', 'core' ), 'title' => 'edit', 'link' => 'app=members&module=members&section=members&do=edit&id=' . $row['member_id'], ), 'delete' => array( 'icon' => array( 'icons/delete.png', 'core' ), 'title' => 'delete', 'link' => 'app=members&module=members&section=members&do=delete&id=' . $row['member_id'], 'class' => 'delete', ), ); }; /* Display */ IPSOutput::i()->output = IPSOutput::i()->getTemplate( 'global' )->block( 'members', $table );
  8. When developing, modifying the database schema (such as adding a column to a table) can be surprisingly time consuming. Currently, we have to: Make the change locally Change the installer Add the query to make the change to the upgrader for whatever version we're working on Let the other developers know so they can run the query to make the change in their installs. This can cause issues, especially at the last step - we currently use a large .sql file in the trunk directory of our repository which we add queries to that we need everyone else to run - sometimes, one of us doesn't notice this has been updated. I'm sure also, third party modification authors are familiar with this annoyance. In 4.0, one of the things I really wanted to do was build a central "Developer Center" from which both us and modification authors can manage aspects of the application without digging into installer/upgrader files, XML files and manually copying and pasting things when creating a new module or extension. Part of this developer centre is a GUI for modifying the database schema. When you make changes, the changes will automatically be ran against your local database and added into the installer and upgrader. Internally, we'll also then have a special script which runs when we SVN update to copy these changes over when another developer makes a change. The first page in the Database Schema Management will be a list of tables: You can add a table to the list either by creating a new table, importing a table which is already in the database or uploading a .sql file containing a CREATE TABLE statement: When you edit a table, you can manage the columns, indexes and rows which are inserted by default: If you try to edit a table and your local database does not match what the schema has, you'll be shown the conflicts: This, plus the other features in the Developer Center, which we'll talk about in later blog entries, make the process of developing applications easier for both us, and third party authors. Fun fact: I'm writing this blog entry from the home of IPS in Lynchburg, VA rather than my usual office in the UK :smile:
  9. Mark posted a blog entry in Invision Community
    Forms are an ubiquitous aspect of any web application. In the IPS Social Suite, particularly in the Admin CP, I often find myself copying and pasting code in various places to create a form. We've had the ipsRegistry::getClass('output')->formInput() and similar methods since 3.0, but you still have to copy all the HTML to display the rows, and write all the code to validate it yourself. Copying and pasting code is something all developers hate. It's a red flag that you're probably doing something wrong. In IPS 4.0, we've written a central form building helper class to alleviate this. Just as a reminder: Everything in this blog is a work in progress - naturally someone with a much keener design sense than I will be going over the interface - I'm just demonstrating the functionality, not a finished product :smile: The Basics Let's say I want to create a form with a single text input field. I simply initiate the form, add an object representing the input, and then display the form (it has a __toString method) - like so: $form = new IPSHelpersForm(); $form->add( new IPSHelpersFormText( 'name', 'default value' ) ); IPSOutput::i()->output .= $form; The helper will automatically look for language strings with the same name as the form element and use them. If there is a language string with the same name and then "_desc" - it'll use that as the description. So the above code, in the ACP, produces something like this: Required Let's say I want the field to be required - I just pass a third argument indicating so. When the form is submitted, if no value has been provided, it will automatically display an inline error: new IPSHelpersFormText( 'name', '', TRUE ) Options The fourth argument I can pass is an array with options dependent on the type of input field I'm adding. These options may change the way a field is displayed or add additional validation. So for a text field, I can specify the minimum and maximum length (which, naturally takes multibyte languages into consideration): new IPSHelpersFormText( 'name', '', TRUE, array( 'minLength' => 5 ) ) For a number field, I might specify the number of decimal points to round to, which it will do on the fly: new IPSHelpersFormNumber( 'name', 0, TRUE, array( 'decimals' => 2 ) ) Watch Video Or I could add an "Unlimited" checkbox, which is quite common for Admin CP settings: new IPSHelpersFormNumber( 'name', 0, TRUE, array( 'unlimited' => TRUE ) ) Custom Validation If the built-in options don't provide enough validation for a given need, you can pass a lambda function as a 5th argument: new IPSHelpersFormText( 'name', '', TRUE, array(), function( $val ) { if ( $val === 'Bad Value' ) { throw new IPSHelpersFormException( 'That value is not allowed.' ); } } ) Uploads Of course - simple input fields aren't all that can be done. How does drag and drop uploading sound? new IPSHelpersFormUpload( 'name', NULL, FALSE, array( 'multiple' => TRUE ) ) Watch Video (By the way, you'll notice a stripy bar flashes up briefly - this is a real progress bar, but because I'm uploading to localhost it's filling faster than it displays - in practice, you'll see a nice smooth-filling progress bar) Getting the values The form helper will by default (you can override it of course) submit to the same page it's on. You can check if it has been submitted (and passed validation) and obtain the values if so simply by calling the values() method - here's an example: $form = new IPSHelpersForm(); $form->add( ... ); if ( $values = $form->values() ) { // $values contains the values from the form } else { IPSOutput::i()->output .= $form; } The values will be returned in a way that is appropriate to the input type. For example, a text input field will return a string, number input will return either an integer or float, an upload field will return an IPSFile object (or array of them, if you're accepting multiple files), etc.
  10. In 4.0, we have made changes to the database class to make use of prepared statements. For insert and update queries, the syntax is the same as it always has been: IPSDb::i()->insert( 'table', array( 'foo' => 'bar' ) ); However, where previously the database class would try to work out the type of variable passed to it - it now binds these to a prepared statement. The real usefulness of this change though, is apparent when you need to use a where clause. Where previously you'd have to do something like this: $this->DB->buildAndFetch( array( 'select' => '*', 'from' => 'table', 'where' => "foo='" . $this->DB->addSlashes( $foo ) . "'" ) ); You can now do: IPSDb::i()->buildAndFetch( array( 'select' => '*', 'from' => 'table', 'where' => array( 'foo=?', $foo ) ) ); We calculate the datatype based on the variable datatype, so previously where you had to do things like $this->DB->setDataType( 'foo', 'string' ) when you wanted to store a value like '01' - ew) - you can now just cast the variable to whatever datatype you like. For example, if you wanted to ensure that the variable was cast as a string to avoid issues where a user name of '007' was detected as an integer and converted to '7' then you'd use: IPSDb::i()->buildAndFetch( array( 'select' => '*', 'from' => 'table', 'where' => array( 'foo=?', (string) $foo ) ) ); Not only is this easier to type and to read, it ensures that the database class always takes care of escaping things properly.
  11. A while back, we casually mentioned in a blog entry that 4.0 would be next major version after 3.4. Development of 4.0 is underway and we're going to be using this new blog to talk about development as we go. As Brandon mentioned a couple of days ago - the format of these entries is going to be developer-specific. If what we're saying doesn't make much sense right now, we will still be putting announcements up in our main blog when they're finished and ready for everyone to see. Because of that, it's also worth bearing in mind that everything is subject to change. I'm going to be posting code samples, screenshots and so forth - but everything in this blog is a work in progress - not the final product - and that will probably show. With that out the way - let's talk about 4.0! :D The file structure Currently, applications are mostly self contained in their folders (which is either /admin/applications, /admin/applications_addon/ips or /admin/applications_addon/other) however, other files are dotted around in /interface, /public, etc. In 4.0, applications will be completely self-contained within a single /applications directory. An application directory will look something like this: extensions dev css html img js lang admin front (it's "front" rather than "public" now) [*]interface [*]modules[*]setup [*]sources [*]tasks [*]xml You will notice the inclusion of a /dev folder. This will not actually be shipped in production, but rather replaces /cache/lang_cache/master_lang and so forth in the 3.x line. Outside of the applications directory, there will be a "system" directory, which contains core framework classes. Namespaces and autoloading In 4.0, we'll be making use of PHP namespaces and using an autoloader. The best way to demonstrate how this works is with a few examples:classDb (/ips_kernel/classDb.php) is now IPSDb and located in /system/Db/Db.php output (/admin/sources/classes/output/publicOutput.php) is now IPSOutput and located in /system/Output/Output.php class_forums (/admin/applications/forums/sources/classes/forums/class_forums.php) is now IPSforumsForum and located in /applications/forums/sources/Forum/Forum.php IPSDispatcherFront and IPSDispatcherAdmin are two new classes (with similar functionality to ipsController in 3.x) and both extend IPSDispatcherDispatcher - all 3 are located in /system/Dispatcher/ in individual files. Better framework design Where appropriate, classes are being refactored to make better use of appropriate design patterns. One lovely side-effect of this is ipsRegistry no longer exists. Instead of, for example ipsRegistry::DB() you now use IPSDb::i() - the Db class uses a multiton design pattern (I didn't pass any arguments in that example, which doing will cause the Db class to load conf_global.php and create the default database connection, but I could have passed it a key) - the i method in this case will create the database connection if it doesn't already exist. To give another example - IPSMember (the new IPSMember) uses an Active Record pattern. So there's no more of this: IPSMember::isInGroup( 1, 4 ); It's now, the much more logical: IPSMember::load( 1 )->isInGroup( 4 ); Monkey patching hooks One of the great things about the IPS Community Suite is hooks - you can easily create a class and instruct the framework to use that instead of a core class. Now, I don't know about you, but I really, really, really hate having to do this: $class = IPSLib::loadLibrary( '/path/to/file', 'myClass' ); $object = new $class; Especially if what you want to call is a static method, in which case it can't be done. You want to of course, just be able to do: $object = new myClass; or: myClass::myStaticMethod(); There is a concept in software engineering to do this sort of thing, called monkey patching, and by clever use of the autoloader, we've managed to make this work. loadLibary and loadActionOverloader are no more. These points are of course, just the beginning of 4.0. Stay tuned for more :smile:
  12. Mark posted a blog entry in Invision Community
    Last year, we added an auto-install/upgrade tool into our client area. The tool would upload the latest version of the IPS Community Suite to your server automatically. The way it worked was after submitting the request, you were entered into a queue, and a program on our server uploaded the files, and emailed you when it was done so that you could run the upgrader script. It normally took about 30 minutes. 30 minutes is pretty good. For someone without the technical knowledge to upgrade, it's a great alternative to asking for a support agent to do the upgrade for you (especially just after a release when everyone is doing just that). However, many users still prefer to do the upgrade themselves (after all, if you can download the source files and upload them to your server in the same time, what's the point?). We decided we should try and get that time down. We also wanted the process to be much more seamless - so you click "Upgrade", get a loading screen, and then are taken immediately to the upgrade script when it's done, rather than waiting for an email. Interested to see how fast we managed to get it? Take a look for yourself: Watch Video In addition, the system can now handle communities that have renamed their admin directory seamlessly without issue (it previously reset to /admin), and the system will automatically detect which encoding of IP.Nexus to use for your server, if applicable (previously it would only use Zend). We really hope the Auto-Upgrader will provide a much easier way for you to keep your community up-to-date with the latest features and enhancements. If you've not already upgraded to IP.Board 3.4, why not go and try it out now? To access the tool, simply go to the Purchases area of the client area, select the community you want to upgrade and click the big "Upgrade Now" button.
  13. As we wrap up principle development of new features for IP.Board 3.4, I wanted to go through some of the other changes we've implemented for the new release. Share a single post A common feature request has been the ability to share a single post. You might want to share a great reply to a topic on Facebook or perhaps email a link to an interesting post to a friend. You can now do this by clicking either the post number or the little share icon to the right of the post number. This brings up a new modal window System Templates Some "system templates" such as the wrapper used for HTML emails were previously only editable by manually changing the files which contained those templates. This is inconvenient, especially as it means one has to remember not to upload those files when upgrading IP.Board. In IP.Board 3.4, these templates will be editable from the Admin CP making it much easier to maintain edited versions of these templates. Mobile Moderation Our mobile theme is suitable for all mobile devices. Even though we have our mobile application available for the iPhone and iPad, we want to ensure that other devices are able to interact with IP.Board. A very common request has been to enable basic moderation actions from within the mobile theme. To that end, we've added buttons for common topic moderation into the mobile skin. Editor Pasting A lot of the time you find that you just want to paste plain text into the editor so you don't have to then remove formatting such as background and font colors. We've made this an option from within the IP.Board 3.4 editor via the new Options icon (far right on the screen shot). You can still paste as rich text by clicking on the relevant paste button on the top right section of the toolbar. We hoped that you've enjoyed reading this series of blogs on IP.Board 3.4. We're currently putting the finishing touches to this major new release and can't wait make it available for release!
  14. IP.Board has for a long time allowed administrators to send bulk mails to members, including the ability to filter recipients and use variables to customise the message sent to each member. This is an important tool for communicating with the members of your community and in 3.4 we've made it even better. The problem with bulk mail Sending vast amounts of email through your own server is troublesome. Firstly, it takes a long time (you can't just send out thousands of emails in one go) and due to the way PHP works, you need to have activity on your community to initiate the sending of each batch of emails (you could set up a cron, but you'd probably only want it running when you actually have a bulk mail sending, and they're fiddly to set up). Secondly, if you're on shared hosting, other sites on the same server may have given your server a bad reputation and caused it to be placed on blacklists, this causes the emails you send to be more likely to be marked as junk. Thirdly, sending vast amounts of email through your own server is an expensive task - most communities we see use the same server for sending emails as hosting the community itself, meaning resources are being used for the sending of those emails and not serving your users. Fortunately, these problems are well-known throughout the internet and a number of companies offer services to send mail for you, through their servers to alleviate these problems. In addition, these services provide web applications where you can view statistics, and track how many of your emails have been opened, rejected, etc. We're really pleased to announce that in IP.Board 3.4 we've built in integration with Mandrill, a service of well-known and respected MailChimp. How it works Setting up integration with Mandrill is really easy. In the new "Community Enhancements" section of the Admin CP (which we've mentioned in earlier blog entries), there will be an option for Mandrill: After creating an account on their site, all you do is enter your account details: IP.Board will from then on send all bulk mails through the Mandrill service. And it's not just bulk mails. You can even configure IP.Board's normal email settings to send all outgoing emails via Mandrill's SMTP server. Sending bulk mails As part of our improvements, we've also: Tidied up the interface for sending bulk mails Improved the unsubscribe link sent in emails to be a one-click link (rather than requiring users sign in and uncheck the box) Built extension capabilities the filter options available when composing a bulk mail, meaning 3rd party applications can add their own filter options (we'll use this for example, in a future version of IP.Nexus to allow you to send bulk mails to anyone who's purchased a particular item). Added a tab on the confirmation screen to allow you to view the list of recipients before actually sending the bulk mail. All these improvements are present even if you choose not to use the new Mandrill integration. Statistics and Tracking You can view statistics via the Mandrill web application. Emails sent from IP.Board's bulk mail system automatically enable tracking for opens and clicks. They even have iPhone and Android apps available. Pricing Full pricing details are available from Mandrill - for up to 12,000 emails per month though, the service is completely free.
  15. A Content Delivery Network, or CDN is a distributed system of servers to provide high-availablity and high-performance. For example, rather than serving your CSS, images and Javascript from the same webserver that PHP and the rest of your community runs on, these are loaded from a network of servers, improving performance and reducing load on your server. Having a CDN can improve your community's quality, reliability and scalability, as well as reduce your hosting costs. By offloading the serving of images, CSS, Javascript, etc. you free up your system resources to serve the real content which makes for a better experience. Some say that there are also SEO advantages o CDN use. The IPS Community Suite has supported Content Delivery Networks for some time. However, they can be expensive and difficult to set up. Many clients want a CDN but do not know where to go and we hope to help them out. We are pleased to announce a new service, IPS CDN, which will allow you to quickly, easily and inexpensively start using this important technology on your community. How does it work? The IPS CDN service will be supported in IP.Board 3.4. After upgrading, you'll notice a new section of the Admin Control Panel called "Community Enhancements" - one of the options available here is "IPS CDN". On this page you'll be able to enable the CDN, which will take you to a new page in the client area where you can purchase credits: Note that the packages shown in this screenshot are examples only. As you will notice from that screenshot, you can purchase credits for your CDN account as and when you want, and optionally set up your account to automatically top-up as you run low on credit. Once this is done, your community will automatically start using the CDN service. If you do not set up automatic top-up, we'll send you an email when your account is running low on credit (when you go below 10GB, 5GB and 1GB). If your credit runs out without you topping up, your community will automatically stop using the CDN service - there will be no interruption to the running of your community, it will automatically notice there's no credit remaining and go back to serving resources locally. If you top up again, it will automatically enable itself again. One problem of using the IPS Community Suite (or indeed any application) with a CDN is that when a resource is changed locally, the CDN needs to be recached to notice the new changes. Unless the CDN is notified of a change it will keep serving the old copy of the file - sometimes for up to 24 hours. This can cause much confusion to you and your visitors. To remedy that problem: since editing skins and the CSS is done from the Admin CP, the system will automatically call the CDN service to recache resources as and when you change them, so you don't need to worry about this. You can keep track of your usage in the Admin CP, where you'll be able to see a graph with your usage over the last 7 days. You'll also be able to buy more credits, disable the system, and manually recache. How much does it cost? The service will be based on "Pay As You Go" pricing. Meaning there's no minimum sign up fee - you simply purchase credit on your account and that credit is good for an amount of data transferred through the service. As you use the CDN, your credit will decrease and you can top up with more. The base price for the CDN will be at or around $0.18 per GB of bandwidth served through the CDN and may drop as you reach higher levels. We realize this is rather unspecific right now but we are negotiating with CDN providers to get the best bulk pricing. It will be our goal to pass any volume savings we receive on to our clients as we look at this service is a great way to enhance our client's experience and want to encourage its use. Everyone will however be offered their first Gigabyte for free to try out the service before purchase. Of course if you do not want to use the IPS CDN service and want to use another provider you can certainly do that. We believe the click and go setup for the IPS CDN service will encourage usage and benefit all IPS Community Suite users. Future CDN Integration Right now the CDN is a basic "pull" implementation whereby it simply pulls data from your live server then serves it via its cache. In the future we hope to implement storage services. This would mean that uploaded files would be stored on the CDN rather than your local computer. This is great to reduce storage costs on your hosting and also means that, other than the actual processing of data, your community's files are geographically distributed. For our power users this would also mean even easier cluster/cloud hosting. Storage and other CDN integrations will come in future versions of the IPS Community Suite so for now enjoy the current features while we work on even more great additions!
  16. VigLink is a service which works with a number of affiliates to provide you commission when a user visits the affiliate from your website. So, as an example, let's say someone posts a link to an Amazon item on your community, with VigLink integration, you'll be provided with compensation every time a user purchases that item if they bought it after following the link from your site. VigLink, and services like it, have been a popular addition for many of our clients and we have received requests for built in integration. Monetizing your community is something IPS has been working to enhance support for over the recent years. From ad code integration spots to our full commerce system IP.Nexus our clients who are interested in monetizing their community have benefited greatly. The addition of VigLink enhances those offerings. This video provides more information on how VigLink works: We've been working closely with VigLink to provide direct integration with this service in IP.Board 3.4. Setting it up couldn't be easier - we've built a new page in the Admin CP called "Community Enhancements" which lists all of the services we integrate with. You simply click on "VigLink" in this page and you'll be taken to the VigLink site where you can either sign up, or log in with your existing VigLink account, if you have one: Once that's done - you're all setup. You don't need to copy and paste any code - the integration will be enabled for you, and you can now track your usage through your VigLink account. There's a number of settings you can configure for finer control over how the system works. You can set which users VigLink applies to (both in terms of who gets redirected through them and whose posts get VigLink enabled) and you can disable the system on a per-forum basis: We'll also be releasing an update to our iOS app around the 3.4 release to support this integration too. IPS benefits financially through a reciprocal marketing agreement when someone uses VigLink through our software however we do not take any cut of the commission offered from VigLink to you. Anyone familiar with IPS knows that taking a cut is not how we would operate :smile: Community Enhancements Section Mentioned earlier in this entry, the AdminCP has a new section called Community Enhancements. This area contains all the various external services that your IPS Community Suite can use to enhance your community. It's not all new though: we have moved services like our Spam Monitoring Service to this section as that too is an external enhancement to your community. We are also introducing some new services that will integrate with your community in this section! Certain services are from external partners, like VigLink, that clients may opt to use and some are new IPS-created services. Some services are free and some carry a small cost depending on the nature of what's being offered. They are all of course optional and if you do not enable them they do not execute or engage any resources in the Suite. Keep an eye out for future blog entries introducing these new Community Enhancements!
  17. The IPS Community Suite provides a fantastic community solution for all kinds of websites. For many of our customers, the community is just one component of their site. Many of these customers utilise single sign-on systems for integrating the community with the other areas. At IPS, we get requests for this on a regular basis, and over the years, I've worked on many of these solutions as part of my day-to-day workload. Companies like Evernote and Roxio have created a single sign-on solution with the IPS Community Suite and their existing user databases. Other companies which manage large numbers of communities like the NFL and the NHL have created a single sign-on solution allowing all their communities to share the same user database. Creating these systems can be quite arduous though. Every setup has different ways of handling data and systems must be created bespoke to each situation. 6 years ago, we had an idea to simplify this. What if we could create a solution that would allow a network of web applications to share user information? We created a solution and called it IP.Converge. Over these 6 years though, the internet has changed. IP.Converge was designed to be a "master" in a network of "Converge compatible" applications. This approach had two main shortcomings: firstly, it is often the case that our software needs to be the "slave" in a single sign-on network, secondly, the approach was too general which made both facilitating full single sign-on (where users are automatically logged into all applications after logging into one) was difficult, and making non-IPS software compatible with Converge was extremely difficult. Fortunately, we think we've come up with a better solution. As of IP.Board 3.4, we are completely removing support for IP.Converge, and have developed a new system, which we're calling IPS Connect. IPS Connect has no central application. In an IPS Connect network, one of the applications will serve as the master, and there will be any number of slaves working off it. When writing IPS Connect, we had three main objectives: So what does this mean? As of IP.Board 3.4, it will be easy, and completely seamless to create a single sign-on network between 2 or more IP.Boards, and 3rd party developers will also be able to write support for any other web application to join in in the network. How does it work? For the simplicity of this example, let's say you're networking 2 IP.Board installations. The "master" installation has a secret key which will be given the "slave" installation. When a user visits the "slave" installation, IP.Board will check if they are logged into the "master" installation - if they are it will log them in automatically, creating the account if necessary. If they're not logged in, but then choose to log in on the "slave" installation - they will automatically be logged into the "master" installation. This happens transparently, without the user leaving the "slave" installation. When a user registers or updates their account, the "master" application will be pinged and updated. Again, this happens transparently. How simple is it to write support for my custom web application? Really simple! If you want your application to be the "master", all you need to do is create a single php file which "slave" applications will send requests to. This needs to do things like facilitate log ins, account registrations, etc. If you want your application to be the "slave", you simply ping that file on the "master" application when stuff happens. We've created 2 completely functional example websites which demonstrate exactly how to do this, which will be available to download when 3.4 is released. [*]Single Sign-On must be completely automatic and effortless. After logging into any application in the network, the user should be automatically logged into all others. And similarly, after logging out, the same. [*]The process should be completely transparent to the user. The user should be able to register an account, or update account information on any application in the network, and these changes should be pushed transparently to the other applications. [*]It should be easy for developers to make their web applications compatible with IPS Connect - and they should be able to make their web applications serve as either the master or the slave.
  18. IP.SEO is a popular free add-on for IP.Board which provides many additional SEO features for your community. IP.SEO has proved so popular since it's introduction that as of IP.Board 3.4, we're going to be moving it's features into the core of IP.Board meaning that a separate application will no longer be necessary. If you're not already familiar with IP.SEO, here's an overview the features it provides, all of which will be included in IP.Board 3.4: Sitemap XML site maps provide a great way to show search engines the content on your community. All IPS applications are supported, and third party applications can also add their own support. You can even specify a "priority" for each forum and other areas of your community to tell search engines which content is more important. IP.Board will automatically update your site map as new content is generated and will automatically ping search engines to let them know your site map has been updated. SEO Advice A new screen in the Admin CP will provide advice on areas of configuration which can be adjusted to provide the best SEO. Search Activity You will now be able to view graphs showing the visits your community has received from search engine spiders, and visits received from users who found your community via a search engine. You can even see the term that users searched for which led them to your community. Meta Tags You can now edit the page title and meta tags for any page on your community. You can also use a "Live Meta Tag Editor" which will allow you to browse your community, adding tags to the pages as you visit them. Miscellaneous FeaturesIP.Board will now be able to automatically ping XML-RPC ping services whenever a new topic is created. You will now be able to select a particular skin which will be used by guests. You can add Google Analytics tracking code through a setting in the Admin CP to integrate with the Google Analytics service. More to come on SEO... this is not the only SEO change being made in 3.4 so keep watching our company blog!
  19. Hopefully most of you don't have the need to visit our bug tracker enough to have noticed, but we recently switched from a third party application for managing our bugs to IP.Content. The history of the software we used to use for our bug tracker goes back quite a bit. It was originally written by Matt, many years ago - before that we used regular forum topics as bug reports! A couple of years ago, around the time IP.Board 3.0 was released, we handed over the codebase to a group of volunteers who wanted to develop the bug tracker application further - and they did. The developers have done a fantastic job with Tracker, however since then we've released IP.Content, our own application which lets you create (amongst other things) custom databases. We decided that since moving forward it's definitely easier for us to be using our own applications (otherwise we have issues whenever we upgrade this site to beta software) especially as soon we'll be developing them all under one release cycle, and IP.Content is more than capable of handling the task, to switch from Tracker to IP.Content. So what did we do to turn IP.Content into a fully featured bug tracker? Actually, nothing special! IP.Content's databases already have support for categories (we created one for each product) and custom fields (we created fields for "status", "version" and "fixed in version"). We didn't need to do any skinning either - the default database templates work fantastic for a bug tracker. We also took this opportunity to have one of our non-technical members of staff create the database. This gave us some great ideas on improving the system for ease of use. In fact, there was only 3 hurdles we had to overcome: 1. Filter Bars When you're working through bug reports you only really want to see open bug reports - we needed a way to filter the records being shown created on the custom "status" field we'd created. After thinking about the best approach for this, we realised that there's probably other uses for IP.Content which also might benefit from fields being "filterable". So we went ahead and added the feature to IP.Content. When the next version of IP.Content is released, any enumerable field type (dropdowns, radio boxes, checkboxes) will be able to be selected as "filterable". When viewing records you'll then see a sidebar allowing you to select one or more of those values to filter the records by. This is what it looks like in our new bug tracker: 2. Quick Changing Fields Unlike most database uses, in a bug tracker, you need to be able to change the record's data (status, fixed in version) quickly - and often at the same time as making a comment on the report. We decided for this we'd need to write some code specifically for our needs. Fortunately, IP.Board's hook system makes this really easy. In case anyone out there is feeling particularly geeky and wants to know how this works, the hook has 3 parts: A skin overloader which adds dropdown boxes above the comment form A data hook which intercepts the comment being added and updates the records data based on the dropdown box values. It also edits the comment that was posted and adds in "Updating Status to: X", or whatever was changed. An action overloader which stops an error being thrown if the comment is empty. In total, the hook is just 81 lines of code and took about 30 minutes to write. 3. Moving the data Our final hurdle was converting the data (for which I whipped up a CLI script). Naturally, we also had to make sure that old bug report links gave HTTP 301 redirects to the new reports - wouldn't want our SEO to suffer! To do this, I just dropped a few lines of code in one of the old Tracker source files. So here it is, our brand new bug tracker, completely powered by IP.Content. Read more about IP.Content or try it for free. If you have any questions about what IP.Content can do, shoot us an email - we'd love to hear from you.
  20. To wrap up our blog entries on IP.Nexus, there's a few more additional features and enhancements that we haven't previously covered: Custom Advertisement Zones IP.Nexus allows you to create advertisements and even sell advertisement space on your community. There are several different areas where the IPS Community Suite has built-in advertisement spots where Nexus can place advertisements. In IP.Nexus 1.5, you can create your own custom "zones" and then add tags into your skin templates, IP.Content pages or even external websites outside of your community to display the advertisements in the zones you have created. Expected Output Monitoring IP.Nexus can monitor your servers and report to you if any of them are not responding. Sometimes though, a site can be having issues, but not actually be reporting as down - for example, if a database table had collapsed, the server would be reporting as online, even though the site isn't actually usable. In IP.Nexus 1.5, you'll be able to create custom "Expected Output Monitoring" rules. IP.Nexus will then call the URLs you provide at regular intervals, and if the page output is not what is expected, will notify you that there's an issue. Transaction Review Details on the items being purchased is now available in the transaction review screen without having to click into the invoice. Voiding Customers IP.Nexus has a tool to quickly ban a customer, refund their transactions, close their support requests, etc. Sometimes however, you might want to only perform a select few of these actions. In IP.Nexus 1.5, each "action" now appears as an individual toggle when you go to void a customer. Resource Improvements When we work on a new release of any application, we look at the resource usage to see if any improvements can be made. We've made several changes to improve the resource usage of IP.Nexus. The most significant of these changes is a change to how package data is cached. Some communities use IP.Nexus with lots of packages in many categories. Up until now, IP.Nexus stored all packages in a cache so that they didn't need to be fetched from the database on every load. However, for communities with lots of categories and packages, this can result in a large cache which ends up using more resources to process than is necessary. In IP.Nexus 1.5 we've reworked some of the logic to make this method of caching data no longer necessary, and as a result, Nexus is able to scale much more efficiently when there is a large number of packages. This wraps up all of the changes we're making in IP.Nexus 1.5. Keep an eye on our pre-release testing forum over the next couple of weeks for public betas :smile:
  21. Our last few blog entries on Nexus have talked about some of the larger changes we've been making to the storefront and how billing is handled. In this blog entry, I wanted to go over some of the smaller changes we've made in these areas :smile: Gift Vouchers Starting with Nexus 1.5, customers can purchase gift vouchers, which can then be redeemed in your store. If you have enabled this, when in the store, they'll see two new options underneath the package categories: Customers will then be able to select a gift voucher amount from the options you have set in the Admin CP, or, if you have enabled it, enter their own amount: \ Vouchers can either be emailed straight to the recipient or printed off. The voucher is given in the form of a code, which can then be entered into the store to redeem. Nexus will keep track of who used vouchers purchased by whom and will show all this information in the normal customer history screen. And of course, all this can be done from the mobile skin too: Multiple Billing Cycles IP.Nexus allows you to create recurring packages with whatever renewal terms you specify. Sometimes though, you might want to give several different options - for example, let's say you wanted a package which costs $10 per month or $100 a year (or whatever). In IP.Nexus 1.5, this is now possible. You can create as many different renewal options as you like: When a customer goes to purchase a package which has more than one renewal option, a drop down will be displayed for them to select their desired renewal option: Self Purchase Management You're probably very familiar with getting support requests from customers who want to renew their purchase for a period of time in advance (we, for example, often get people asking to pay for a year's hosting) or cancel their purchase. In IP.Nexus (provided you have enabled it), customers will be able to renew purchases for any length of time up to what you have allowed, and cancel their own purchases. Bulk Discounts Nexus currently has several options for providing discounts to certain customers: Usergroup discounts can be used to give customers in certain groups a discount. Loyalty discounts can be used to give customers who already own (or are purchasing) some of your products a discount. Bundle discounts can be used to give customers a discount on their entire purchase if certain items are purchased in combination. In Nexus 1.5, we're adding a new discount type which we call Bulk discounts. Bulk discounts can be used to give customers a discount on an individual product if they are purchasing it with certain other products. In this manner, it can be used for "Buy one get one free" type deals: In this example, the first of this item the customer adds to their cart will be full price, the second will be free, the third will be full price, the fourth will be free, and so on. You could also do "Buy one get second half price" or "3 for the price of 2" style deals, and so on. Grouped Renewals Let's say you have a setup where you want to adjust the renewal fees for a product based on additional products that a customer has purchased. Currently, there are two ways to do this. You can set up add-ons as product options which adjust the renewal price of the package - however this isn't feasible if the add-ons also need access to support, downloads, etc. Alternatively you could set the add-ons up as packages which must be associated with a parent and have their own renewal costs - however, this means that if a customer purchases an add-on at a later date, their renewals won't be at he same time, which can be inconvenient and confusing. In IP.Nexus 1.5 we're adding the option for packages which can be associated with a parent package to have grouped renewals. This means that rather than having their own renewal dates, the renewal fees for add-on packages will be added on to the parent package. Latest Products The storefront in Nexus allows you to view featured products, popular products or browse by category. In IP.Nexus 1.5, we've added an option to view the latest products. Thumbnail Sizes The thumbnails used in the IP.Nexus store are hardcoded to 100x100px. In IP.Nexus 1.5 you'll be able to set whatever thumbnail size you'd like to use. When you adjust the setting, Nexus will set a flag on your products that the thumbnails need to be rebuilt, and this will be done on the fly as they're requested, so you don't need to sit around waiting for the images to rebuild.
  22. IP.Nexus allows you to sell hosting packages with using integration with CPanel/WHM. In IP.Nexus 1.5 we're adding support to sell domain names too using integration with eNom. Once set up, customers purchasing hosting accounts will see a new option to purchase a domain name: The TLDs available and their prices can be set in the Admin CP. When selecting this option, the domain will be added to the cart like a normal item: When the customer then pays, the domain will be registered through eNom. The domain then shows as a normal purchase associated with the hosting account: A renewal invoice will be generated as normal when the domain is due to expire, and when renewals are paid, the expiry date will be updated with eNom. This feature requires an eNom reseller account. In future versions of IP.Nexus we will add additional registrars based on interest.
  23. IP.Nexus currently provides support for a number of gateways: Authorize.Net (with ARB and CIM support), PayPal (Website Payments Standard and Pro), 2CheckOut and manual payments (check, bank wire, etc.) In IP.Nexus 1.5 we're adding more gateways and extending the functionality of some existing gateways. Gateways Stripe Stripe is a relatively young payment gateway. They support all major cards and have no monthly or setup fees which makes them great for smaller companies wanting to accept card payments. One of the more interesting aspects of Stripe is that they have a clever solution where although the customer can enter their card details right in Nexus, these details never actually hit your server - which is great for security. Those interested in the technicalities of this - the card form elements are displayed without having a 'name' attribute, meaning that their values are not sent in the POST data to your server. Instead, Javascript on Stripe's server extracts these details and provides Nexus with a token which is what is then sent. In addition, Stripe supports "storing" card details (again, technically, Nexus only actually stores a token) meaning when paying your customers will be able to tick a box to save card details, and these details can automatically be used for renewals, future purchases, etc. Authorize.Net DPM Nexus already provides support for Authorize.Net's AIM (that's "Advanced Integration Method") service. This allows customers to enter card details directly into Nexus. The way this works is that the card details have to be sent to Nexus - this is a concern for some as it means that, even though it's very briefly, your server has the customer's card information, which means that certain security precautions need to be considered. Authorize.Net now have a new service called DPM ("Direct Post Method") - to the customer, this seems to work exactly the same, however, rather than posting the card data to your server, and then Nexus sending that data to Authorize.Net, the form submits directly to Authorize.Net's server - meaning your server never deals with the sensitive card information. Unfortunately, due to the way this works, Authorize.Net's ARB and CIM services (both of which are a form of recurring billing) are not compatible with DPM. Those who need recurring payments can keep using the AIM method. Sagepay Nexus 1.5 also adds support for Sagepay. Refunds Currently, Nexus allows you to refund transactions right from the Admin CP for Authorize.Net and PayPal transactions. In IP.Nexus 1.5, PayPal Pro and 2CheckOut can now handle this too. Authorize.Net DPM and Stripe, which are added in 1.5, also support it. 2CheckOut Improvements 2CheckOut payments are now handled with their INS service. Not only is this more secure and reliable, using it has also allowed us to add support for recurring payments. So where does that leave us? IP.Nexus now supports 7 different payment gateways (8 if you include manual payments like check and bank wire), each of which have their own features and merits over one another. We've created a little table to show the key differences between each gateway: As always, we'll continue to gauge interest and add additional gateways to Nexus over time. But wait.... there's more! Split Payments Sometimes, people might want to pay for an invoice using two different cards (or other payment methods). In IP.Nexus 1.5, this is now possible: You can set a minimum invoice amount before this feature is available, or disable it completely if you don't want to use it. Package Methods In IP.Nexus 1.5, you'll now be able to set which payment methods can be used to purchase a particular package:
  24. IP.Nexus allows you to set up shipping prices with customisable rates, destinations and tax. Once an item has been shipped, you can then provide a tracking number which will provide the customer with a link where they can view tracking information. While this solution allows complete control over shipping prices, it can be tedious to set up methods, and arrange shipments with couriers - particularly if you receive lots of orders. In IP.Nexus 1.5, we're introducing direct integration with FedEx. In future versions of IP.Nexus we plan to integrate with other providers like UPS and USPS in a similar way. The setup To get started, you simply need to enter your FedEx account details in the Admin CP. You can also specify which services to use (if you don't want to use them all), add a surcharge to FedEx prices and more: Once that's done - you're all setup. FedEx options will automatically become available when a customer selects a shipping method - you just need to make sure you have provided an accurate weight for shippable products. You can of course disable FedEx services for any individual product. Purchase When a customer goes to purchase a product they'll now see FedEx services in the shipping methods select box: You'll notice that each option shows the price as well as an estimated delivery date. This data is coming straight from FedEx based on the customer's shipping address and the items being purchased. The customer can also edit their shipping address on this screen which will adjust the prices accordingly - the shipping address can be different from the billing address provided in step 1 (but will be automatically filled out with that data). The address entered is also validated with FedEx to check for any mistakes in filling it out. Shipping In the Admin CP, you'll now see a button on the shipping order to arrange the shipment: Clicking this will show you a screen like this: As you can see, there are several options for how you want to provide the package to FedEx. The nearest FedEx drop boxes and service stations are listed and clicking on any will provide the opening hours: You can also have Nexus schedule a courier to come to you to pick up a package which will present you with this screen: Once shipment has been arranged you'll be able to print a shipping label. When the package has been delivered, you'll be able to see a proof-of-delivery with the signature that signed for the package. Both of these are provided by FedEx: Tracking The shipping order page in the Admin CP provides tracking information from FedEx: The customer also has a page in the client area where they can view the same information:
  25. Our last two blog entries have focussed on large new features to the support system in IP.Nexus. But we're not stopping there! In this blog entry I wanted to talk about some of the smaller new features and enhancements we've made to the support system for IP.Nexus 1.5. Pay-Per-Incident IP.Nexus 1.5 adds the ability for you to define a support department as "Pay-Per-Incident". Each department allows you to enter a Pay-Per-Incident amount. When a customers tries to create a support request in this department, they'll see a message like this: Clicking the "Submit Request" button will take them straight to the payment screen to provide payment for the support request. After submitting payment, they'll be redirected straight back to the support request submission form - the title and department will even be remembered and filled in: Contact Us Nexus allows guests to create support requests which can be used for sales enquires, etc. Currently, this is usually done through the incoming email support, however in 1.5, we've added an optional "Contact Us" link to the footer which will allow anyone, including guests, to submit a support request: Tracking notifications IP.Nexus allows staff members to "track" support requests, tracked requests can be accessed quickly from the main support request list and are highlighted in that list. In IP.Nexus 1.5 we've also added the option to receive email notifications when a customer replies to support requests you are tracking: Staff members can reply to these notifications directly via email and their reply will be added to the support request. Reply to notifications IP.Nexus allows you to send a notification to any email address(es) for new support requests or emails within a particular department. Previously, the notification just said that a support request had been created or replied to, and gave you the title and ID number. In IP.Nexus 1.5, the notification email will contain the full body of the message and can be replied to directly by email. Improved Incoming Email Handling IP.Nexus supports the ability to receive incoming emails as support requests and replies. In IP.Nexus 1.5 we've rewritten a lot of this code to make it more reliable and secure. We've also added a setting to allow you to control whether you prefer Nexus to parse emails as HTML or in plaintext. Each message now also has a toggle which allows you to quickly toggle (it uses AJAX) between HTML or plaintext on any message: Merge Staff members have been able to split a support request into two requests in Nexus for a long time. In 1.5, we've added the option you can merge two together into one. Reminder Emails IP.Nexus allows you to assign support requests to staff members. Staff members can quickly see all support requests assigned to them from the support request list, however, if you don't regularly check the list, you may not know a request has been assigned to you. In IP.Nexus 1.5, if a staff member has any support requests assigned to them, they'll receive an email each morning with a list of the support requests that are assigned to them. Edit Title In IP.Nexus 1.5, staff members will be able to edit the title of a support request: Stock Actions When Logging a Support Request Stock Actions allow you to quickly set a department, status, assigned staff member and reply on a support request. In IP.Nexus 1.5, when logging a support request in the Admin CP on behalf of a customer, you'll be able to use these Stock Actions in addition to when replying.

Account

Navigation

Search

Search

Configure browser push notifications

Chrome (Android)
  1. Tap the lock icon next to the address bar.
  2. Tap Permissions → Notifications.
  3. Adjust your preference.
Chrome (Desktop)
  1. Click the padlock icon in the address bar.
  2. Select Site settings.
  3. Find Notifications and adjust your preference.